Skip to content

Conversation

@ljonesfl
Copy link
Member

@ljonesfl ljonesfl commented Nov 29, 2025

Upgrades the blog editor to be a block based editor.
A ton of refactoring.

Summary by CodeRabbit

  • New Features

    • Editor.js rich post editor with image support, shortcode/widget rendering, admin image & featured-image upload endpoints, Cloudinary media uploader and upload validation; CMS upgrade CLI (cms:upgrade).
  • Database / Migrations

    • New migrations: content_raw for posts, two-factor recovery codes and per-user timezone; content stored as Editor.js JSON.
  • Security

    • Auth events (login/logout, password reset, email verified) and combined auth+CSRF filter.
  • Documentation

    • New migration guide, upgrade notes and release manifest.
  • Tests

    • Large expansion of unit and integration tests across content, media, auth, publishing and widgets.

✏️ Tip: You can customize this high-level summary in your review settings.

@ljonesfl
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 29, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 29, 2025

Walkthrough

Updated namespaces to Neuron\Data\Settings; added Editor.js content storage and renderer; Cloudinary media uploader (with SSRF checks), validator, DTO and admin endpoints; many repositories moved to ORM; added events, Auth+CSRF filter, cms:upgrade CLI, new migrations, tests, CI and docs.

Changes

Cohort / File(s) Summary
Composer & CI
composer.json, .github/workflows/ci.yml, .gitignore, readme.md
Dependency changes (neuron-php/data, cloudinary), added dev dep and scripts, CI switched to setup-php with coverage upload, added coverage.xml to .gitignore and README badge.
Namespace bulk updates
src/Bootstrap.php, resources/app/Initializers/*, src/Cms/**, tests/**
Replaced imports from Neuron\Data\Setting\*Neuron\Data\Settings\* across code and tests.
Editor.js content & model/migrations
resources/views/admin/posts/create.php, resources/views/admin/posts/edit.php, resources/views/blog/show.php, resources/database/migrate/20251119224525_add_content_raw_to_posts.php, src/Cms/Models/Post.php
Admin UIs switched to EditorJS with hidden JSON field; new content_raw column and migration; Post model stores JSON, exposes get/set, and extracts plain-text body.
Media upload (Cloudinary)
src/Cms/Services/Media/IMediaUploader.php, src/Cms/Services/Media/CloudinaryUploader.php, src/Cms/Services/Media/MediaValidator.php, src/Cms/Controllers/Admin/Media.php, src/Cms/Dtos/MediaUploadDto.yaml, resources/config/routes.yaml
New media uploader interface and Cloudinary implementation (file/URL upload, delete), SSRF/DNS checks, validator and DTO, admin endpoints returning Editor.js or JSON; routes added with auth-csrf.
Rendering, shortcodes & widgets
src/Cms/Services/Content/EditorJsRenderer.php, src/Cms/Services/Content/ShortcodeParser.php, src/Cms/Services/Widget/WidgetRenderer.php, src/Cms/Controllers/Blog.php
Added EditorJs renderer, shortcode parser and widget renderer; Blog controller now renders Editor.js content and shortcodes.
Auth, CSRF & filters
src/Cms/Services/Auth/CsrfToken.php, src/Cms/Auth/Filters/CsrfFilter.php, src/Cms/Auth/Filters/AuthCsrfFilter.php, resources/app/Initializers/AuthInitializer.php
CsrfToken now accepts IRandom, CsrfFilter uses filtered POST extraction, new AuthCsrfFilter enforces auth+CSRF and is registered in initializer.
Events & maintenance
src/Cms/Events/*, src/Cms/Maintenance/MaintenanceManager.php, resources/app/Initializers/MaintenanceInitializer.php
New event classes (login/logout/failures, password/email, maintenance enabled/disabled); MaintenanceManager emits events and guards JSON parsing.
ORM migration: repositories & connection
src/Cms/Repositories/..., src/Cms/Database/ConnectionFactory.php
Many repositories moved from raw PDO to ORM usage (with PDO retained for some complex joins); connection factory imports adjusted.
Randomness injection & API renames
multiple src/Cms/Services/*Creator.php, *Updater.php, Auth/EmailVerifier.php, Auth/PasswordResetter.php, Auth/CsrfToken.php
Introduced IRandom injection (RealRandom default) replacing uniqid()/random_bytes; constructors updated and many public methods renamed from $body$content.
CLI changes & provider
src/Cms/Cli/Commands/Install/UpgradeCommand.php, src/Cms/Cli/Provider.php, (removed) src/Cms/Cli/Commands/Generate/EmailCommand.php, src/Cms/Cli/Commands/Queue/InstallCommand.php
Added cms:upgrade command (migration copying/manifest update); removed mail:generate and queue:install command classes; provider registrations updated.
Migrations, manifest & docs
resources/database/migrate/*, resources/.cms-manifest.json, MIGRATIONS.md, UPGRADE_NOTES.md, versionlog.md
New migrations (content_raw, two-factor + timezone), CMS manifest file, migration/upgrade documentation and upgrade notes added.
Media DTOs, views & examples
src/Cms/Dtos/*.yaml, resources/views/http_codes/500.php, resources/config/*.example
New DTO YAMLs added; static 500 view added; some example config files removed/updated; routes and config examples extended for cloudinary/email/maintenance.
Tests & test infra
tests/Integration/*, tests/Unit/*, tests/phpunit.xml, tests/TESTING.md
Added IntegrationTestCase and many new integration/unit tests (media, EditorJS, validators, services); PHPUnit config updated and testing docs added.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant CMS
    participant Cloudinary
    participant DB

    User->>Browser: Compose post in EditorJS (may embed image)
    Browser->>CMS: POST /admin/upload/image (multipart)
    CMS->>CMS: MediaValidator.validate(file)
    alt invalid
        CMS-->>Browser: 400 EditorJS error JSON
    else valid
        CMS->>Cloudinary: UploadApi.upload(file)
        Cloudinary-->>CMS: upload response (url, public_id, meta)
        CMS-->>Browser: 200 EditorJS success JSON
    end

    Browser->>CMS: POST /admin/posts (content_raw JSON + metadata)
    CMS->>CMS: Creator.setContent(content_raw) → extractPlainText → setBody
    CMS->>DB: INSERT/UPDATE post (content_raw, body, ...)
    DB-->>CMS: success + id
    CMS-->>Browser: Redirect / success

    Browser->>CMS: GET /blog/{slug}
    CMS->>DB: SELECT post (content_raw)
    DB-->>CMS: content_raw JSON
    CMS->>CMS: EditorJsRenderer.render(content_raw)
    CMS->>CMS: ShortcodeParser.parse(rendered HTML)
    CMS-->>Browser: 200 rendered page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Focus areas:

  • Repository ORM conversions, relation sync and transaction safety (DatabasePostRepository: syncCategories/syncTags, transactions).
  • Editor.js JSON storage ↔ plain-text extraction correctness and migration compatibility (Post model, content_raw migration).
  • CloudinaryUploader: SSRF/DNS resolution, upload/delete flows, option merging and error handling.
  • Event emissions and side-effects in Authentication, PasswordResetter, EmailVerifier, and MaintenanceManager.
  • UpgradeCommand idempotence and file-manipulation safety when copying migrations and updating installed manifest.
  • New integration test infra (Migration execution, transactional isolation) and CI test impacts.

Possibly related PRs

Poem

🐇 I nibbled bytes and stitched a thread,

JSON blocks and images fed,
Tokens hum and events hop near,
ORM burrows tidy, tests appear,
Carrots, upgrades — hop, cheer! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.53% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/blog editor upgrade' clearly describes the main change: upgrading the blog editor to a block-based system, which is central to this large changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/blog-editor-upgrade

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Cms/Services/Auth/Authentication.php (1)

226-249: Fix infinite recursion on stale sessions in user() / logout()

In user():

if( !$user )
{
	// Clear stale session if user no longer exists
	$this->logout();
}

If the session contains a user_id for a user that has been deleted, this calls logout(), which in turn calls $this->check() and $this->user(), triggering the same path again and causing infinite recursion (now with additional event logic on each loop).

Instead of delegating to logout(), clear the stale session inline:

-		$user = $this->_userRepository->findById( $userId );
-
-		if( !$user )
-		{
-			// Clear stale session if user no longer exists
-			$this->logout();
-		}
-
-		return $user;
+		$user = $this->_userRepository->findById( $userId );
+
+		if( !$user )
+		{
+			// Clear stale session and remember-me cookie without recursion
+			$this->_sessionManager->destroy();
+
+			if( isset( $_COOKIE['remember_token'] ) )
+			{
+				setcookie( 'remember_token', '', time() - 3600, '/', '', true, true );
+			}
+
+			return null;
+		}
+
+		return $user;

This preserves the intended behavior (dropping an invalid session and remember-me cookie) without risking a stack overflow or runaway event emissions when the underlying user record is missing.

src/Cms/Controllers/Blog.php (1)

87-93: "Not found" post will have empty content for rendering.

When a post is not found, a dummy Post object is created with setBody() but the new rendering pipeline uses getContent() which will return the default empty EditorJS structure {"blocks":[]}. This means the "not found" message won't be displayed.

Update the "not found" post to use the new content structure:

 		if( !$post )
 		{
 			$post = new Post();
 			$post->setTitle( 'Article Not Found' );
-			$post->setBody( 'The requested article does not exist.' );
+			$post->setContent( json_encode([
+				'blocks' => [
+					[
+						'type' => 'paragraph',
+						'data' => ['text' => 'The requested article does not exist.']
+					]
+				]
+			]) );
 			$post->setSlug( $slug );
 		}
🧹 Nitpick comments (16)
resources/views/http_codes/500.php (2)

8-20: Inline styles are appropriate for error pages.

Using inline styles ensures the page displays correctly even if external stylesheets fail to load, which is a good practice for error pages.

Optional: Consider adding responsive design for smaller screens:

 	.centered {
 		text-align: center;
-		margin-top: 100px; /* Adjust this value as needed */
+		margin-top: 10vh;
+		padding: 0 15px;
 	}
 
 	.centered-text {
 		text-align: center;
 		margin-top: 20px;
 		font-size: 16px;
 		color: #666;
+		padding: 0 15px;
 	}

1-6: Consider adding accessibility attributes and user escape hatches.

The error page would benefit from:

  • ARIA landmark roles for screen readers
  • A link back to the home page or contact information
-<div class="row">
-	<div class="col">
+<div class="row" role="main">
+	<div class="col">
 		<h1 class="centered">Something went wrong on our end.</h1>
 		<p class="centered-text">The server encountered an unexpected error. Please try again later.</p>
+		<p class="centered-text"><a href="/" style="color: #0066cc;">Return to Home</a></p>
 	</div>
 </div>
src/Cms/Repositories/DatabaseCategoryRepository.php (1)

211-217: GROUP BY may break on PostgreSQL

allWithPostCount() selects c.* but groups only by c.id; PostgreSQL’s stricter GROUP BY rules will reject this query. If you intend this repository to work against PostgreSQL (per the class doc), you’ll need to either:

  • Group by all selected categories columns, or
  • Select only grouped/aggregated columns.

Please confirm whether PostgreSQL is a supported target in your deployments; if so, consider adjusting the SELECT/GROUP BY to be portable.

composer.json (1)

15-27: Dependency updates align with the Settings refactor; confirm version ranges

The move to neuron-php/mvc: "0.9.*" and addition of neuron-php/data: "0.9.*" and neuron-php/scaffolding: "0.8.*" fit the new Settings namespace and tooling story.

One nuance: "0.9.*" can allow earlier 0.9.x releases than ^0.9.5 would have; in practice Composer will usually pick the latest matching version, but if you depend on features/bugfixes introduced in later 0.9.x, you may prefer a caret or a higher minimum.

Please make sure composer.lock is updated and CI (including migrations/tests that touch Settings) passes with these new constraints. If you want to avoid accidentally downgrading, consider tightening the constraints (e.g., ^0.9.5 / ^0.9.0) before merging.

src/Cms/Repositories/DatabasePostRepository.php (1)

84-103: Verify handling of the new content_raw column in posts

The new migration adds a non‑nullable content_raw column intended for Editor.js JSON, but this repository currently:

  • Inserts/updates only body/excerpt (no content_raw in the column/value lists), and
  • Does not include content_raw in the $data array passed to Post::fromArray().

Because the DB default will populate content_raw, this won’t break inserts, but any Editor.js JSON stored on the entity may not be persisted or hydrated unless other layers handle it.

Please double‑check how the Post model exposes raw Editor.js content (e.g., getContentRaw() / setContentRaw() or similar), and consider extending:

  • the INSERT/UPDATE statements, and
  • mapRowToPost()’s $data array
    to read/write content_raw if that field is meant to be first‑class in the domain.

Also applies to: 141-168, 440-455

src/Cms/Services/Auth/PasswordResetter.php (1)

106-110: LGTM! Event emissions enhance observability.

The addition of PasswordResetRequestedEvent and PasswordResetCompletedEvent provides valuable audit trails for password reset workflows. The fallback to 'unknown' for $_SERVER['REMOTE_ADDR'] appropriately handles CLI and testing contexts.

Consider importing the event classes at the top of the file rather than using fully qualified names for improved readability:

+use Neuron\Cms\Events\PasswordResetRequestedEvent;
+use Neuron\Cms\Events\PasswordResetCompletedEvent;
+use Neuron\Application\CrossCutting\Event;

Then use:

-		\Neuron\Application\CrossCutting\Event::emit( new \Neuron\Cms\Events\PasswordResetRequestedEvent(
+		Event::emit( new PasswordResetRequestedEvent(

Also applies to: 175-179

resources/views/admin/posts/edit.php (2)

24-31: Harden handling of existing EditorJS content JSON before initialization

Current parsing only falls back on JSON syntax errors; valid-but-unexpected JSON (e.g. "null" or an object without blocks) will flow into data and may confuse EditorJS.

You can defensively ensure a valid blocks array after parsing:

-let existingContent;
-try {
-	existingContent = JSON.parse(existingContentJson);
-} catch (error) {
-	console.error('Failed to parse existing content:', error);
-	existingContent = { blocks: [] };
-}
+let existingContent;
+try {
+	existingContent = JSON.parse(existingContentJson);
+
+	// Ensure we always pass a valid Editor.js data structure
+	if( !existingContent || !Array.isArray( existingContent.blocks ) )
+	{
+		existingContent = { blocks: [] };
+	}
+} catch (error) {
+	console.error('Failed to parse existing content:', error);
+	existingContent = { blocks: [] };
+}

This keeps the editor resilient to legacy or malformed content_raw values while preserving your existing fallback behavior.

Also applies to: 93-103, 104-109


82-90: Consider pinning EditorJS CDN versions and reducing save() frequency

The EditorJS integration and submit flow look solid. Two optional robustness points:

  • The @latest CDN URLs can introduce breaking changes when a new major EditorJS (or tool) version ships. Consider pinning to a specific major/minor version (e.g. @2.x or an exact version) and upgrading deliberately.
  • onChange currently calls editor.save() on every change, which is relatively heavy. If this becomes a performance issue for long posts, you could debounce/throttle the save into #content-json or rely solely on the submit handler to serialize content.

Both are optional, but would make the editor behavior more predictable in production.

Also applies to: 145-149, 151-163

tests/Cms/Services/Post/CreatorTest.php (1)

45-69: Centralize EditorJS content JSON in a helper/fixture for test readability

The tests correctly assert both getContentRaw() and the derived getBody(). The same EditorJS JSON pattern ({"blocks":[{"type":"paragraph","data":{"text":"Body"}}]} and variants) is repeated across multiple tests, though.

To keep things DRY and easier to tweak later, consider extracting small helpers/constants, e.g.:

private function makeEditorJsContent(string $text = 'Body'): string
{
	return json_encode([
		'blocks' => [
			[
				'type' => 'paragraph',
				'data' => ['text' => $text],
			],
		],
	]);
}

Then use makeEditorJsContent('Test body content') / makeEditorJsContent() throughout the tests instead of hardcoding JSON strings.

Also applies to: 90-95, 118-123, 148-153, 177-182, 218-223, 263-273, 297-305

src/Cms/Maintenance/MaintenanceManager.php (1)

61-82: Emit maintenance events without risking core enable/disable operations

The new enabled/disabled events are wired in cleanly, and the optional $disabledBy argument keeps the public API backward compatible.

One robustness refinement to consider: if any event listener hooked into MaintenanceModeEnabledEvent / MaintenanceModeDisabledEvent throws, callers of enable() / disable() will see an exception even though the maintenance file has already been written or deleted. To keep state changes reliable, you might wrap the emits:

-		$result = $this->writeMaintenanceFile( $data );
-
-		// Emit maintenance mode enabled event
-		if( $result )
-		{
-			\Neuron\Application\CrossCutting\Event::emit( new \Neuron\Cms\Events\MaintenanceModeEnabledEvent(
-				$data['enabled_by'],
-				$message
-			) );
-		}
+		$result = $this->writeMaintenanceFile( $data );
+
+		// Emit maintenance mode enabled event (non-fatal)
+		if( $result )
+		{
+			try
+			{
+				\Neuron\Application\CrossCutting\Event::emit(
+					new \Neuron\Cms\Events\MaintenanceModeEnabledEvent(
+						$data['enabled_by'],
+						$message
+					)
+				);
+			}
+			catch( \Throwable $e )
+			{
+				// Optional: log but do not affect enable() result
+			}
+		}

and similarly in disable(). That keeps maintenance mode toggling deterministic even if downstream listeners misbehave.

Also applies to: 87-107

src/Cms/Services/Auth/Authentication.php (1)

41-59: Event emissions for login attempts look good; consider small refactors

The new UserLoginFailedEvent, UserLoginEvent, and UserLogoutEvent hooks are well-placed and give you good observability over auth flows. A couple of low-friction refinements:

  • The four failure branches emit UserLoginFailedEvent with only the reason differing. You could extract a small private helper like emitLoginFailed(string $username, string $reason): void to reduce duplication and keep changes localized if you later enrich the payload.
  • In login(), you call microtime( true ) both when setting login_time in the session and when emitting UserLoginEvent. Reusing a single timestamp would keep those values perfectly aligned:
-		// Store user ID in session
-		$this->_sessionManager->set( 'user_id', $user->getId() );
-		$this->_sessionManager->set( 'user_role', $user->getRole() );
-		$this->_sessionManager->set( 'login_time', microtime( true ) );
+		// Store user ID in session
+		$this->_sessionManager->set( 'user_id', $user->getId() );
+		$this->_sessionManager->set( 'user_role', $user->getRole() );
+		$loginTime = microtime( true );
+		$this->_sessionManager->set( 'login_time', $loginTime );
@@
-		// Emit user login event
-		\Neuron\Application\CrossCutting\Event::emit( new \Neuron\Cms\Events\UserLoginEvent(
-			$user,
-			$_SERVER['REMOTE_ADDR'] ?? 'unknown',
-			microtime( true )
-		) );
+		// Emit user login event
+		\Neuron\Application\CrossCutting\Event::emit( new \Neuron\Cms\Events\UserLoginEvent(
+			$user,
+			$_SERVER['REMOTE_ADDR'] ?? 'unknown',
+			$loginTime
+		) );

Overall the behavior is sound; these are just to keep things DRY and metrics-friendly.

Also applies to: 61-85, 87-110, 134-156, 161-201

tests/Cms/Services/Post/UpdaterTest.php (1)

39-42: Mirror CreatorTest by centralizing EditorJS JSON for updates

These tests correctly validate that Updater::update() preserves both the raw EditorJS JSON (getContentRaw()) and the derived body text (getBody()), and that other fields (slug, status, categories, tags, etc.) behave as expected.

As in CreatorTest, you might want to centralize the repeated JSON snippets into a small helper (on this class or a shared trait) so any future changes to your EditorJS block shape only need to be made in one place.

Also applies to: 54-70, 97-99, 127-128, 171-172, 218-219, 254-255, 285-286, 321-322

resources/views/admin/posts/create.php (1)

131-142: Consider potential issues with programmatic form submission.

The submit handler prevents the default form submission, saves editor content, then calls e.target.submit(). This bypasses normal HTML5 validation and could cause issues if there are other submit handlers or validation logic.

Consider using FormData to submit the form programmatically while maintaining validation:

-document.getElementById('post-form').addEventListener('submit', async (e) => {
-	e.preventDefault();
-
-	try {
-		const savedData = await editor.save();
-		document.getElementById('content-json').value = JSON.stringify(savedData);
-		e.target.submit();
-	} catch (error) {
-		console.error('Error saving editor content:', error);
-		alert('Error preparing content. Please try again.');
-	}
-});
+document.getElementById('post-form').addEventListener('submit', async (e) => {
+	// Don't prevent default initially - let HTML5 validation run first
+	if (!e.target.checkValidity()) {
+		return; // Let browser handle validation
+	}
+	
+	e.preventDefault();
+	
+	try {
+		const savedData = await editor.save();
+		document.getElementById('content-json').value = JSON.stringify(savedData);
+		e.target.submit();
+	} catch (error) {
+		console.error('Error saving editor content:', error);
+		alert('Error preparing content. Please try again.');
+	}
+});
src/Cms/Controllers/Blog.php (1)

106-108: Add error handling for content rendering.

The renderer should handle malformed JSON or rendering errors gracefully to prevent the page from breaking when post content is invalid.

Wrap the rendering in a try-catch block:

-		// Render content from Editor.js JSON
-		$content = $post->getContent();
-		$renderedContent = $this->_renderer->render( $content );
+		// Render content from Editor.js JSON
+		$content = $post->getContent();
+		try {
+			$renderedContent = $this->_renderer->render( $content );
+		} catch (\Exception $e) {
+			error_log("Error rendering post content for slug '{$slug}': " . $e->getMessage());
+			$renderedContent = '<p>Content temporarily unavailable.</p>';
+		}
src/Cms/Models/Post.php (2)

125-128: Add error handling for malformed JSON in getContent().

If _contentRaw contains invalid JSON, json_decode() will return null and the ?? operator will correctly return the default, but this happens silently. Consider logging the error for debugging.

 	public function getContent(): array
 	{
-		return json_decode( $this->_contentRaw, true ) ?? ['blocks' => []];
+		$decoded = json_decode( $this->_contentRaw, true );
+		if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
+			error_log("Invalid JSON in Post content_raw (ID: {$this->_id}): " . json_last_error_msg());
+		}
+		return $decoded ?? ['blocks' => []];
 	}

607-646: Consider XSS risks in extractPlainText() for 'raw' block type.

Line 633 includes raw HTML from raw blocks without sanitization when extracting plain text. While strip_tags() is applied at line 640, malicious HTML could still be present in the intermediate processing.

Since this is meant to be plain text extraction, consider whether raw blocks should be included at all, or strip tags immediately:

 			$blockText = match( $block['type'] )
 			{
 				'paragraph', 'header' => $block['data']['text'] ?? '',
 				'list' => isset( $block['data']['items'] ) && is_array( $block['data']['items'] )
 					? implode( "\n", $block['data']['items'] )
 					: '',
 				'quote' => $block['data']['text'] ?? '',
 				'code' => $block['data']['code'] ?? '',
-				'raw' => $block['data']['html'] ?? '',
+				'raw' => strip_tags($block['data']['html'] ?? ''),  // Strip tags immediately for raw HTML
 				default => ''
 			};
 
 			if( $blockText !== '' )
 			{
 				// Strip HTML tags from text
 				$blockText = strip_tags( $blockText );
 				$text[] = trim( $blockText );
 			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67ec356 and 9ebd824.

📒 Files selected for processing (61)
  • composer.json (2 hunks)
  • resources/app/Initializers/AuthInitializer.php (1 hunks)
  • resources/app/Initializers/MaintenanceInitializer.php (1 hunks)
  • resources/app/Initializers/PasswordResetInitializer.php (1 hunks)
  • resources/app/Initializers/RegistrationInitializer.php (1 hunks)
  • resources/database/migrate/20251119224525_add_content_raw_to_posts.php (1 hunks)
  • resources/views/admin/posts/create.php (3 hunks)
  • resources/views/admin/posts/edit.php (3 hunks)
  • resources/views/blog/show.php (2 hunks)
  • resources/views/http_codes/500.php (1 hunks)
  • src/Bootstrap.php (1 hunks)
  • src/Cms/Cli/Commands/Generate/EmailCommand.php (0 hunks)
  • src/Cms/Cli/Commands/Generate/stubs/email.stub (0 hunks)
  • src/Cms/Cli/Commands/Install/InstallCommand.php (1 hunks)
  • src/Cms/Cli/Commands/Maintenance/EnableCommand.php (1 hunks)
  • src/Cms/Cli/Commands/Queue/InstallCommand.php (0 hunks)
  • src/Cms/Cli/Provider.php (0 hunks)
  • src/Cms/Controllers/Admin/Categories.php (1 hunks)
  • src/Cms/Controllers/Admin/Profile.php (1 hunks)
  • src/Cms/Controllers/Admin/Users.php (1 hunks)
  • src/Cms/Controllers/Blog.php (4 hunks)
  • src/Cms/Controllers/Content.php (4 hunks)
  • src/Cms/Database/ConnectionFactory.php (1 hunks)
  • src/Cms/Email/helpers.php (1 hunks)
  • src/Cms/Events/EmailVerifiedEvent.php (1 hunks)
  • src/Cms/Events/MaintenanceModeDisabledEvent.php (1 hunks)
  • src/Cms/Events/MaintenanceModeEnabledEvent.php (1 hunks)
  • src/Cms/Events/PasswordResetCompletedEvent.php (1 hunks)
  • src/Cms/Events/PasswordResetRequestedEvent.php (1 hunks)
  • src/Cms/Events/UserLoginEvent.php (1 hunks)
  • src/Cms/Events/UserLoginFailedEvent.php (1 hunks)
  • src/Cms/Events/UserLogoutEvent.php (1 hunks)
  • src/Cms/Maintenance/MaintenanceConfig.php (1 hunks)
  • src/Cms/Maintenance/MaintenanceManager.php (1 hunks)
  • src/Cms/Models/Post.php (5 hunks)
  • src/Cms/Repositories/DatabaseCategoryRepository.php (1 hunks)
  • src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1 hunks)
  • src/Cms/Repositories/DatabasePageRepository.php (1 hunks)
  • src/Cms/Repositories/DatabasePasswordResetTokenRepository.php (1 hunks)
  • src/Cms/Repositories/DatabasePostRepository.php (1 hunks)
  • src/Cms/Repositories/DatabaseTagRepository.php (1 hunks)
  • src/Cms/Repositories/DatabaseUserRepository.php (1 hunks)
  • src/Cms/Services/Auth/Authentication.php (5 hunks)
  • src/Cms/Services/Auth/EmailVerifier.php (2 hunks)
  • src/Cms/Services/Auth/PasswordResetter.php (3 hunks)
  • src/Cms/Services/Email/Sender.php (1 hunks)
  • src/Cms/Services/Member/RegistrationService.php (1 hunks)
  • src/Cms/Services/Post/Creator.php (3 hunks)
  • src/Cms/Services/Post/Updater.php (3 hunks)
  • tests/Cms/BlogControllerTest.php (1 hunks)
  • tests/Cms/Cli/Commands/Install/InstallCommandTest.php (1 hunks)
  • tests/Cms/ContentControllerTest.php (1 hunks)
  • tests/Cms/Maintenance/MaintenanceConfigTest.php (1 hunks)
  • tests/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (1 hunks)
  • tests/Cms/Repositories/DatabaseUserRepositoryTest.php (1 hunks)
  • tests/Cms/Services/AuthenticationTest.php (1 hunks)
  • tests/Cms/Services/EmailVerifierTest.php (1 hunks)
  • tests/Cms/Services/PasswordResetterTest.php (1 hunks)
  • tests/Cms/Services/Post/CreatorTest.php (9 hunks)
  • tests/Cms/Services/Post/UpdaterTest.php (10 hunks)
  • tests/Cms/Services/RegistrationServiceTest.php (1 hunks)
💤 Files with no reviewable changes (4)
  • src/Cms/Cli/Provider.php
  • src/Cms/Cli/Commands/Generate/EmailCommand.php
  • src/Cms/Cli/Commands/Queue/InstallCommand.php
  • src/Cms/Cli/Commands/Generate/stubs/email.stub
🧰 Additional context used
🧬 Code graph analysis (15)
src/Cms/Events/PasswordResetRequestedEvent.php (2)
src/Cms/Events/EmailVerifiedEvent.php (2)
  • __construct (29-33)
  • getName (35-38)
src/Cms/Events/PasswordResetCompletedEvent.php (2)
  • __construct (30-35)
  • getName (37-40)
resources/views/admin/posts/create.php (1)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Events/MaintenanceModeDisabledEvent.php (2)
src/Cms/Events/EmailVerifiedEvent.php (2)
  • __construct (29-33)
  • getName (35-38)
src/Cms/Events/MaintenanceModeEnabledEvent.php (2)
  • __construct (30-35)
  • getName (37-40)
src/Cms/Events/UserLoginEvent.php (3)
src/Cms/Events/EmailVerifiedEvent.php (2)
  • __construct (29-33)
  • getName (35-38)
src/Cms/Events/UserLoginFailedEvent.php (2)
  • __construct (31-38)
  • getName (40-43)
src/Cms/Events/UserLogoutEvent.php (2)
  • __construct (30-35)
  • getName (37-40)
src/Cms/Events/UserLoginFailedEvent.php (3)
src/Cms/Events/EmailVerifiedEvent.php (2)
  • __construct (29-33)
  • getName (35-38)
src/Cms/Events/UserLoginEvent.php (2)
  • __construct (31-37)
  • getName (39-42)
src/Cms/Events/UserLogoutEvent.php (2)
  • __construct (30-35)
  • getName (37-40)
src/Cms/Maintenance/MaintenanceManager.php (2)
src/Cms/Events/MaintenanceModeEnabledEvent.php (1)
  • MaintenanceModeEnabledEvent (24-41)
src/Cms/Events/MaintenanceModeDisabledEvent.php (1)
  • MaintenanceModeDisabledEvent (24-39)
tests/Cms/Services/Post/CreatorTest.php (2)
src/Cms/Controllers/Content.php (1)
  • getTitle (141-144)
src/Cms/Models/Post.php (2)
  • getTitle (74-77)
  • getContentRaw (133-136)
src/Cms/Events/MaintenanceModeEnabledEvent.php (2)
src/Cms/Events/EmailVerifiedEvent.php (2)
  • __construct (29-33)
  • getName (35-38)
src/Cms/Events/MaintenanceModeDisabledEvent.php (2)
  • __construct (29-33)
  • getName (35-38)
src/Cms/Services/Post/Creator.php (1)
src/Cms/Models/Post.php (1)
  • setContent (142-147)
src/Cms/Services/Auth/EmailVerifier.php (2)
src/Cms/Events/EmailVerifiedEvent.php (1)
  • EmailVerifiedEvent (24-39)
src/Cms/Services/Auth/Authentication.php (1)
  • user (226-249)
resources/views/admin/posts/edit.php (2)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Models/Post.php (2)
  • getId (57-60)
  • getContentRaw (133-136)
src/Cms/Services/Auth/Authentication.php (6)
src/Cms/Events/UserLoginFailedEvent.php (1)
  • UserLoginFailedEvent (23-44)
src/Cms/Models/User.php (2)
  • isLockedOut (332-340)
  • isActive (187-190)
src/Cms/Auth/SessionManager.php (1)
  • set (99-103)
src/Cms/Events/UserLoginEvent.php (1)
  • UserLoginEvent (24-43)
src/Cms/Controllers/Auth/Login.php (1)
  • logout (139-143)
src/Cms/Events/UserLogoutEvent.php (1)
  • UserLogoutEvent (24-41)
resources/database/migrate/20251119224525_add_content_raw_to_posts.php (1)
src/Cms/Services/Post/Updater.php (1)
  • update (48-83)
tests/Cms/Services/Post/UpdaterTest.php (2)
src/Cms/Models/Post.php (5)
  • setContent (142-147)
  • getTitle (74-77)
  • getContentRaw (133-136)
  • getBody (108-111)
  • getStatus (246-249)
src/Cms/Repositories/DatabasePostRepository.php (1)
  • update (127-187)
src/Cms/Services/Post/Updater.php (1)
src/Cms/Models/Post.php (1)
  • setContent (142-147)
🔇 Additional comments (54)
src/Cms/Maintenance/MaintenanceConfig.php (1)

5-5: Namespace migration is complete and verified.

The import change from Neuron\Data\Setting to Neuron\Data\Settings on line 5 is correct. Verification confirms no remaining old namespace references exist in the codebase, and the new namespace is consistently used across 20 files (production code and tests).

src/Cms/Cli/Commands/Maintenance/EnableCommand.php (1)

8-8: Yaml settings source namespace update looks correct

The Yaml import now targets Neuron\Data\Settings\Source\Yaml, which matches how it’s used in loadConfiguration() and aligns with the Settings namespace migration; no further changes needed here as long as the installed neuron-php/data version exposes this namespace.

src/Cms/Services/Email/Sender.php (1)

7-7: SettingManager import aligned with Settings namespace

Switching to Neuron\Data\Settings\SettingManager is consistent with the property/ctor type hints and the rest of the Settings migration; the email config access remains unchanged.

src/Cms/Services/Member/RegistrationService.php (1)

9-9: RegistrationService now correctly targets Settings\SettingManager

The import of Neuron\Data\Settings\SettingManager matches the constructor and property types and keeps the member settings lookups intact.

src/Cms/Controllers/Admin/Categories.php (1)

11-11: Categories controller import updated to Settings\SettingManager

The controller now imports Neuron\Data\Settings\SettingManager, keeping it consistent with the rest of the Settings namespace migration in the project.

tests/Cms/Maintenance/MaintenanceConfigTest.php (1)

6-6: Test Yaml source import matches new Settings namespace

Using Neuron\Data\Settings\Source\Yaml in this test is consistent with the production MaintenanceConfig usage and the broader Settings namespace move.

src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1)

7-7: Repository constructor now depends on Settings\SettingManager

The repository correctly imports Neuron\Data\Settings\SettingManager and passes it to ConnectionFactory::createFromSettings(), keeping database configuration behavior unchanged under the new namespace.

tests/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (1)

8-8: Test mock updated to Settings\SettingManager

The test now mocks Neuron\Data\Settings\SettingManager, matching the repository’s constructor signature and ensuring the database config wiring remains valid.

tests/Cms/ContentControllerTest.php (1)

6-6: In-memory settings source switched to Settings\Source\Memory

The test now uses Neuron\Data\Settings\Source\Memory, which is consistent with the Settings namespace refactor and how the controller reads settings via the Registry.

src/Cms/Repositories/DatabaseUserRepository.php (1)

7-7: Repository constructor now aligned with Settings namespace

The repository’s dependency on SettingManager is correctly switched to the Neuron\Data\Settings namespace, keeping the constructor type-hint and ConnectionFactory::createFromSettings() usage consistent.

Please run your user-related tests (e.g., DatabaseUserRepositoryTest, auth flows) to confirm there are no remaining references to the old Neuron\Data\Setting\SettingManager type.

src/Cms/Repositories/DatabasePageRepository.php (1)

8-8: Page repository SettingManager import migrated cleanly

The constructor’s SettingManager dependency now points at Neuron\Data\Settings\SettingManager, matching the rest of the PR’s namespace migration without altering any repository behavior.

Confirm that any code instantiating DatabasePageRepository (controllers/services) is also using the updated Settings-namespace SettingManager instance from the container/registry.

src/Cms/Repositories/DatabaseTagRepository.php (1)

7-7: Tag repository settings dependency updated to new namespace

DatabaseTagRepository now correctly imports Neuron\Data\Settings\SettingManager while keeping the constructor contract and internals the same, so behavior should be unaffected.

As part of your test run, ensure any install/seed or tag-related CLI flows still resolve the correct SettingManager implementation.

tests/Cms/Cli/Commands/Install/InstallCommandTest.php (1)

8-9: Install command test imports aligned with Settings namespace

The test now imports Neuron\Data\Settings\SettingManager and Neuron\Data\Settings\Source\Yaml, matching the production code’s updated namespaces and avoiding mismatch with the newer data/settings package layout.

If these imports are no longer needed by the test, they can be safely removed in a later cleanup pass once static analysis (e.g., phpstan/psalm) is green.

src/Cms/Controllers/Admin/Users.php (1)

13-13: Admin Users controller now consistent with Settings-based repositories

The controller’s import of Neuron\Data\Settings\SettingManager matches the updated constructor dependency of DatabaseUserRepository, keeping the Registry-provided 'Settings' instance aligned with the repository’s expected type.

Please verify that the Registry is registering an instance of the new Neuron\Data\Settings\SettingManager class under the 'Settings' key in your bootstrap/initializer code.

src/Bootstrap.php (1)

4-5: Bootstrap imports updated to new data library namespaces

Version and Yaml now import from Neuron\Data\Objects\Version and Neuron\Data\Settings\Source\Yaml, which matches the reorganized data library structure without changing the boot() logic.

After updating composer dependencies, run a full application bootstrap (and any bootstrap-related tests) to confirm there are no unresolved references to the old Neuron\Data\Object\Version or Neuron\Data\Setting\Source\Yaml classes.

src/Cms/Controllers/Admin/Profile.php (1)

10-10: Admin Profile controller settings import matches repository expectations

The profile controller’s import now targets Neuron\Data\Settings\SettingManager, aligning its dependency graph with the updated DatabaseUserRepository and the broader Settings namespace migration.

As with the Users controller, double‑check that the Registry’s 'Settings' entry is an instance of the new Settings\SettingManager to avoid subtle type mismatches at runtime.

src/Cms/Database/ConnectionFactory.php (1)

5-5: Namespace migration verified as complete

The import change to Neuron\Data\Settings\SettingManager is correct and actively used. Verification confirms:

  • No lingering references to the old Neuron\Data\Setting namespace exist
  • The type hint in createFromSettings() (line 26) properly references the new SettingManager class
  • The method implementation correctly delegates to createFromConfig() as expected

The migration is consistent and safe to merge.

src/Cms/Repositories/DatabaseCategoryRepository.php (1)

7-7: Settings namespace update is consistent

Switching to Neuron\Data\Settings\SettingManager here matches the broader Settings namespace refactor and should be a no‑op at runtime as long as the updated neuron‑php/data dependency is installed.

Please run the test suite after composer update with the new dependencies to confirm there are no autoloading or wiring issues around SettingManager.

tests/Cms/Repositories/DatabaseUserRepositoryTest.php (1)

8-8: Updated SettingManager import in tests looks good

Using Neuron\Data\Settings\SettingManager in the mocks keeps the test suite aligned with the refactored Settings namespace; behavior remains unchanged because the class is mocked.

After updating dependencies, please run this test class to ensure the new namespace resolves correctly under your autoloader.

src/Cms/Repositories/DatabasePostRepository.php (1)

10-10: Settings namespace import updated correctly

Referencing Neuron\Data\Settings\SettingManager here keeps this repository in sync with the Settings refactor and ConnectionFactory usage; the constructor contract remains the same.

After dependency updates, please run a small smoke test that instantiates DatabasePostRepository via your usual bootstrap path to confirm the new namespace resolves as expected.

src/Cms/Email/helpers.php (1)

4-4: Email helpers now reference the new Settings namespace

Updating the type to Neuron\Data\Settings\ISettingSource keeps these helpers compatible with the refactored Settings package without changing their runtime behavior.

Please ensure the updated Settings package is present in all environments where these helpers are used (including CLI contexts) and that any existing call sites still pass a compatible settings source.

resources/database/migrate/20251119224525_add_content_raw_to_posts.php (1)

13-22: Migration for content_raw is straightforward; verify adapter support for options

Adding content_raw as non‑nullable TEXT with a default empty Editor.js document is reasonable and should keep existing rows valid.

The use of 'after' => 'body' and 'comment' => 'Editor.js JSON content' is typically MySQL‑specific in Phinx; depending on how strictly you target SQLite/PostgreSQL as well, those options may be ignored or could need adapter‑specific handling.

Please confirm that this migration runs cleanly against all database engines you support (especially in CI and local dev). If you see adapter‑specific issues, you may want to conditionally set after/comment or drop them for non‑MySQL adapters.

src/Cms/Events/EmailVerifiedEvent.php (1)

24-38: EmailVerifiedEvent implementation looks solid

The event cleanly encapsulates the verified User via a readonly promoted property and exposes a clear, stable name ('email.verified') for subscribers. The design is consistent with the other event classes in this PR.

src/Cms/Events/MaintenanceModeEnabledEvent.php (1)

24-40: MaintenanceModeEnabledEvent is consistent with the event model

Readonly constructor‑promoted properties for enabledBy and message plus a stable name 'maintenance.enabled' match the existing disabled‑event pattern and give listeners all relevant context.

src/Cms/Events/UserLoginEvent.php (1)

24-42: UserLoginEvent structure is consistent and ready for consumers

The event payload (User, IP, timestamp) and name user.login align with the other event types you’ve added and give downstream listeners enough context without extra coupling. No issues spotted.

src/Cms/Controllers/Content.php (2)

3-4: Version loading via factory is a clean upgrade

Switching to Factories\Version::fromFile("../.version.json") while keeping the broad \Exception catch preserves existing behavior and centralizes version deserialization in the data library. As long as the working directory is stable for web/CLI entry points, this looks good.

Also applies to: 51-55, 85-94


201-213: Defaulting markdown page route to index improves resilience

Using $request->getRouteParameter('page') ?? 'index' avoids failures when the route omits page and matches the typical “root markdown page” expectation. Just confirm your router/front-controller has an index.md (or equivalent) to back this default.

src/Cms/Repositories/DatabasePasswordResetTokenRepository.php (1)

5-8: Settings namespace refactor is consistent

Using Neuron\Data\Settings\SettingManager here matches the broader Settings namespace migration and keeps the existing constructor contract intact. No repository behavior changes introduced.

Also applies to: 23-32

tests/Cms/Services/PasswordResetterTest.php (1)

11-13: Test settings imports now align with production namespaces

Updating to Settings\Source\Memory and Settings\SettingManager keeps the test configuration in sync with the data layer refactor. The in-memory setup logic remains the same and should continue to exercise PasswordResetter as before.

Also applies to: 34-42

tests/Cms/Services/EmailVerifierTest.php (1)

10-12: EmailVerifier tests correctly track the Settings namespace change

The move to Neuron\Data\Settings\Source\Memory and Settings\SettingManager mirrors the production code and leaves the test behavior intact (test-mode email, same keys). Looks good.

Also applies to: 31-39

resources/app/Initializers/RegistrationInitializer.php (1)

31-38: Registration initializer now guards against the correct Settings manager type

Switching the instanceof check to \Neuron\Data\Settings\SettingManager matches the new Settings namespace and preserves the early-exit behavior when configuration isn’t wired. No additional issues here.

src/Cms/Services/Auth/EmailVerifier.php (1)

10-11: I'll help you verify the review comment about email verification event emission safety. Let me start by exploring the codebase to understand how the Event::emit() method handles missing emitters and listener failures.
<function_calls>


#!/bin/bash

Find the Event class definition

rg -n "class Event" --type=php

Find emit method definitions

rg -n "function emit|public function emit" --type=php -A 5

</function_calls>

src/Cms/Services/Post/Creator.php (1)

35-40: Confirmed: API change is complete and well-implemented internally, but lacks external documentation

The migration is thorough within the repository:

  • Post model correctly implements setContent() with EditorJS JSON, auto-extracting plain text to _body for backward compatibility
  • Creator service properly uses setContent()
  • Admin form is fully updated with Editor.js frontend that serializes to EditorJS JSON
  • Database schema includes content_raw column for JSON storage
  • Tests pass EditorJS JSON correctly

However, this package is publicly distributed via Composer ("neuron-php/cms" library type), making Creator::create() a public API. The breaking change—requiring EditorJS JSON instead of rendered body strings—lacks a migration guide or changelog. No backward compatibility wrapper exists.

tests/Cms/Services/RegistrationServiceTest.php (1)

10-11: LGTM! Namespace migration applied correctly.

The import paths have been updated to use the new Neuron\Data\Settings namespace, aligning with the project-wide refactoring.

resources/app/Initializers/PasswordResetInitializer.php (1)

9-9: LGTM! Namespace migration applied correctly.

The import path has been updated to use the new Neuron\Data\Settings\SettingManager namespace.

src/Cms/Cli/Commands/Install/InstallCommand.php (1)

9-10: LGTM! Namespace migration applied correctly.

The import paths have been updated to use the new Neuron\Data\Settings namespace.

tests/Cms/Services/AuthenticationTest.php (1)

11-11: LGTM! Namespace migration applied correctly.

The import path has been updated to use the new Neuron\Data\Settings\SettingManager namespace.

tests/Cms/BlogControllerTest.php (1)

13-14: LGTM! Namespace migration applied correctly.

The import paths have been updated to use the new Neuron\Data\Settings namespace.

resources/app/Initializers/AuthInitializer.php (1)

32-32: LGTM! Type check updated for namespace migration.

The instanceof check has been updated to reference the new Neuron\Data\Settings\SettingManager namespace.

resources/views/blog/show.php (2)

51-160: LGTM! Comprehensive styling for EditorJS content.

The CSS additions provide proper styling for post content and EditorJS widgets, including headings, lists, blockquotes, code blocks, images, and custom widgets.


39-39: I'll help you verify the concerns raised in the review comment. Let me search the codebase for the Blog controller and examine how $renderedContent is set.
<function_calls>

#!/bin/bash

Find the Blog controller

fd -t f -i "blog" --type=php | head -20


</function_calls>

Let me search more specifically for the Blog controller:
<function_calls>

#!/bin/bash

Search for Blog controller class definition

rg -n "class Blog" --type=php -A 5


</function_calls>

Now let me search for where the show view is rendered:
<function_calls>

#!/bin/bash

Search for show view rendering and renderedContent variable

rg -n "renderedContent|show.php|blog/show" --type=php -B 3 -A 3


</function_calls>

Let me also check the structure of the resources/views/blog directory:
<function_calls>

#!/bin/bash

List files in blog views directory

fd -t f "show.php" && echo "---" && fd -t f "blog" resources/views/ 2>/dev/null | head -10


</function_calls>

Let me check the full repository structure to understand the layout:
<function_calls>

#!/bin/bash

Get an overview of the repository structure

git ls-files | grep -E "(controller|blog)" | head -20


</function_calls>

src/Cms/Services/Auth/PasswordResetter.php (1)

10-10: LGTM! Namespace migration applied correctly.

The import path has been updated to use the new Neuron\Data\Settings\SettingManager namespace.

resources/app/Initializers/MaintenanceInitializer.php (1)

45-59: Updated Settings manager namespace check looks correct

Switching the instanceof check to \Neuron\Data\Settings\SettingManager aligns this initializer with the new Settings namespace without changing runtime behavior. No further changes needed here.

src/Cms/Events/PasswordResetCompletedEvent.php (1)

24-40: PasswordResetCompletedEvent structure and naming are consistent

Readonly payload (User + IP) and the password.reset_completed name match the existing event style in this module. Good to go.

src/Cms/Events/PasswordResetRequestedEvent.php (1)

24-40: PasswordResetRequestedEvent aligns with the existing event model

Readonly User + IP payload and the password.reset_requested name follow the same pattern as your other events. Looks good as a foundation for security monitoring and auditing.

src/Cms/Events/UserLogoutEvent.php (1)

1-41: LGTM!

The event class is well-structured with clear documentation, follows the established event pattern, and properly uses constructor property promotion. The inclusion of sessionDuration is a valuable addition for analytics.

src/Cms/Controllers/Blog.php (1)

40-44: LGTM! Well-structured dependency initialization.

The renderer initialization properly chains dependencies (WidgetRenderer → ShortcodeParser → EditorJsRenderer), creating a clean content processing pipeline.

src/Cms/Events/UserLoginFailedEvent.php (1)

1-44: LGTM!

The event class is well-designed for security monitoring and audit trails. The inclusion of reason, ip, and timestamp provides comprehensive context for failed login attempts, which is essential for security analysis and compliance.

src/Cms/Events/MaintenanceModeDisabledEvent.php (1)

8-39: LGTM! Event structure is appropriate.

The event class is well-documented and follows the established pattern. Using a string for $disabledBy (username/identifier) is consistent with the EnabledEvent and provides flexibility.

src/Cms/Services/Post/Updater.php (1)

39-62: LGTM! Clean migration to EditorJS content handling.

The parameter rename from $body to $content and the corresponding method call change from setBody() to setContent() properly reflect the shift to EditorJS JSON format. The updated docblock clearly documents this change.

src/Cms/Models/Post.php (3)

156-169: LGTM! Good error handling for JSON encoding.

The setContentArray() method properly handles JSON encoding failures with an explicit exception, which is better than silent failures. The use of \JsonException is appropriate.


501-523: Well-designed content handling in fromArray().

The method properly handles both content_raw and content keys, and supports both string and array formats for each. This provides good flexibility for different data sources while maintaining backward compatibility.


625-635: Use match expression correctly handles EditorJS block types.

The match expression properly extracts text from various EditorJS block types. The handling of list items with implode() and the fallback to empty string for unknown types is appropriate.

resources/views/admin/posts/create.php (1)

104-108: The image upload endpoint handler for /admin/upload/image is not implemented in the codebase.

While the codebase has a robust CSRF protection system in place (CsrfFilter and CsrfToken service), the actual route handler for the /admin/upload/image endpoint referenced in the frontend editor configuration (create.php and edit.php) could not be located. The endpoint is configured in the views but no corresponding PHP route handler exists in src/Cms/Controllers/ or elsewhere.

Recommendation: Implement the /admin/upload/image route handler and ensure it is protected with the CsrfFilter to validate CSRF tokens before processing uploads.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Cms/Repositories/DatabaseUserRepository.php (1)

20-32: PDO connection is created but never used.

The $_pdo property is initialized via ConnectionFactory::createFromSettings() but no method in this class uses it. This wastes a database connection. Either remove the PDO initialization or add a comment explaining if it's needed for future use or external access.

-	private ?PDO $_pdo = null;
-
 	/**
 	 * Constructor
 	 *
 	 * @param SettingManager $settings Settings manager with database configuration
 	 * @throws Exception if database configuration is missing or adapter is unsupported
 	 */
 	public function __construct( SettingManager $settings )
 	{
-		// Keep PDO property for backwards compatibility with tests
-		$this->_pdo = ConnectionFactory::createFromSettings( $settings );
+		// ORM is initialized in Bootstrap - no direct PDO needed
 	}
src/Cms/Repositories/DatabasePostRepository.php (1)

260-282: getByCategory and getByTag don't load relations.

Unlike findById and findBySlug, these methods return posts without loading categories and tags. This inconsistency may cause issues when consuming code expects relations to be populated.

-	return array_map( fn( $row ) => Post::fromArray( $row ), $rows );
+	$posts = array_map( fn( $row ) => Post::fromArray( $row ), $rows );
+	foreach( $posts as $post )
+	{
+		$this->loadRelations( $post );
+	}
+	return $posts;

Apply to both getByCategory and getByTag. Note: this introduces N+1 queries; consider batching if performance is critical.

Also applies to: 287-309

♻️ Duplicate comments (1)
resources/views/admin/posts/create.php (1)

99-106: Plugin load check is incomplete (same issue as edit.php).

Similar to edit.php, the plugin availability check only validates EditorJS, Header, and List, but the configuration uses additional plugins (ImageTool, Quote, CodeTool, Delimiter, RawTool).

Apply the same fix suggested for edit.php to ensure all required plugins are validated before initialization.

🧹 Nitpick comments (17)
resources/config/neuron.yaml.example (4)

79-90: Cloudinary config is clear; add a note about handling credentials securely

The structure and defaults look good and match typical Cloudinary usage. To avoid people committing real secrets into VCS, it’s worth explicitly calling that out in the example.

Consider this small tweak:

   cloudinary:
     cloud_name: your-cloud-name  # Your Cloudinary cloud name (required)
     api_key: your-api-key  # API key from Cloudinary console (required)
-    api_secret: your-api-secret  # API secret from Cloudinary console (required)
+    api_secret: your-api-secret  # API secret from Cloudinary console (required)
+    # NOTE: In production, do not commit real Cloudinary credentials to version control.
+    #       Prefer environment-specific config or environment variables for secrets.
     folder: neuron-cms/images  # Default folder for uploads (optional)
     max_file_size: 5242880  # Maximum file size in bytes (5MB default)
     allowed_formats: [jpg, jpeg, png, gif, webp]  # Allowed image formats

Also double‑check that max_file_size and allowed_formats align with whatever validation the uploader middleware/enforcer is using so the UX is consistent.


91-101: Database example looks fine; verify adapter strings match actual driver identifiers

The keys and comments are straightforward and useful. The only potential footgun is the adapter string values and how SQLite is meant to be configured.

  • Confirm that mysql, pgsql, and sqlite exactly match what your DB layer expects (no pdo_mysql/pdo_pgsql vs custom names mismatch).
  • For sqlite, host, port, user, and pass are typically ignored; consider adding a brief comment or a second example to show what a SQLite config should look like to avoid confusion.

110-128: Email/PHPMailer config is well documented; double‑check driver mapping and test mode behavior

The email section is nicely annotated and gives practical SMTP examples. The main risk is drift between these keys and the actual mailer implementation.

  • Ensure that driver: mail|sendmail|smtp maps cleanly to the PHPMailer setup (or equivalent) so users don’t hit silent fallbacks.
  • Confirm that test_mode behaves exactly as described (log but do not send) across all drivers.
  • Optionally, you might add a short note that Gmail typically requires an app password and 2FA, which you already hint at via your-app-password but could call out explicitly in a comment.

129-138: Maintenance config is solid; consider clarifying IP/CIDR support

Defaults here look good (localhost allowlist, clear retry semantics, and optional custom view).

  • If the implementation supports CIDR ranges and IPv6 ranges, it may be worth turning the commented 192.168.1.0/24 example into a one‑line explanation in the comment to make that capability explicit.
  • Double‑check that retry_after is indeed interpreted as seconds (matching HTTP Retry-After) and that custom_view paths are resolved consistently with the rest of the view system.
.github/workflows/ci.yml (1)

10-11: Consider updating actions/checkout to v4.

actions/checkout@v3 is functional but v4 is the current version with performance improvements and Node.js 20 runtime.

     - name: Checkout
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
resources/database/migrate/20251205000000_add_two_factor_and_timezone_to_users.php (1)

45-45: update() is called unconditionally.

When both columns already exist, update() is called without any pending changes. While harmless, you could wrap it in a conditional to avoid the no-op database call.

-		$table->update();
+		if( !$table->hasColumn( 'two_factor_recovery_codes' ) || !$table->hasColumn( 'timezone' ) )
+		{
+			$table->update();
+		}

Alternatively, track whether any column was added and only call update() if needed.

src/Cms/Repositories/DatabasePageRepository.php (1)

24-27: Constructor parameter is unused.

The $settings parameter is no longer used since ORM initialization moved to Bootstrap. Consider either removing the parameter (breaking change) or documenting why it's retained for interface compatibility.

src/Cms/Repositories/DatabasePostRepository.php (1)

411-426: Duplicate code: attachCategories/attachTags overlap with sync methods.

The attach methods duplicate the INSERT logic from syncCategories/syncTags. Consider refactoring to have sync call a shared insert helper, or remove the attach methods if sync is the canonical approach.

Also applies to: 442-457

resources/views/admin/posts/edit.php (1)

119-126: Plugin load check is incomplete.

The interval only checks for EditorJS, Header, and List, but the editor configuration also uses ImageTool, Quote, CodeTool, Delimiter, and RawTool. If any of these fail to load, the editor will initialize but those tools will be unavailable or cause errors.

Consider extending the check to include all required plugins:

 		if (typeof EditorJS !== 'undefined' &&
 		    typeof Header !== 'undefined' &&
-		    typeof List !== 'undefined') {
+		    typeof List !== 'undefined' &&
+		    typeof ImageTool !== 'undefined' &&
+		    typeof Quote !== 'undefined' &&
+		    typeof CodeTool !== 'undefined' &&
+		    typeof Delimiter !== 'undefined' &&
+		    typeof RawTool !== 'undefined') {
resources/views/admin/posts/create.php (1)

118-194: Consider extracting shared EditorJS initialization to a reusable script.

The EditorJS initialization logic is nearly identical to edit.php. While acceptable for view files, extracting the shared initialization code to a dedicated JavaScript file (e.g., /js/admin/editor-init.js) would reduce duplication and simplify maintenance.

src/Cms/Models/Post.php (1)

156-169: Proper error handling in setContentArray.

Using json_last_error_msg() and throwing JsonException provides clear feedback when JSON encoding fails. However, using the JSON_THROW_ON_ERROR flag with json_encode() would be more idiomatic for PHP 7.3+.

 public function setContentArray( array $content ): self
 {
-	$encoded = json_encode( $content );
-
-	if( $encoded === false )
-	{
-		$error = json_last_error_msg();
-		throw new \JsonException( "Failed to encode content array to JSON: {$error}" );
-	}
+	$encoded = json_encode( $content, JSON_THROW_ON_ERROR );

 	$this->_contentRaw = $encoded;
 	$this->_body = $this->extractPlainText( $encoded );
 	return $this;
 }
src/Cms/Services/Media/MediaValidator.php (2)

108-150: MIME type allowlist is hardcoded and doesn't align with configurable allowed_formats.

The allowed_formats setting is configurable and used for extension validation, but the $allowedMimeTypes array is hardcoded. If someone configures additional formats (e.g., svg, bmp), the MIME check will reject them even though the extension check passes. Also, image/jpg is not a valid MIME type (should only be image/jpeg).

Consider deriving the MIME types from the allowed formats or making both configurable:

-		$allowedMimeTypes = [
-			'image/jpeg',
-			'image/jpg',
-			'image/png',
-			'image/gif',
-			'image/webp'
-		];
+		$mimeTypeMap = [
+			'jpg' => 'image/jpeg',
+			'jpeg' => 'image/jpeg',
+			'png' => 'image/png',
+			'gif' => 'image/gif',
+			'webp' => 'image/webp'
+		];
+
+		$allowedMimeTypes = array_values( array_intersect_key( $mimeTypeMap, array_flip( $allowedFormats ) ) );

81-99: Zero-byte check should precede max-size check.

A zero-byte file will trigger both the $size > $maxSize condition (false) and then the $size === 0 condition. While logically correct, checking for zero-byte first provides a more specific error message and is slightly more efficient.

 	private function validateFileSize( int $size ): bool
 	{
+		if( $size === 0 )
+		{
+			$this->_errors[] = 'File is empty';
+			return false;
+		}
+
 		$maxSize = $this->_settings->get( 'cloudinary', 'max_file_size' ) ?? 5242880; // 5MB default

 		if( $size > $maxSize )
 		{
 			$maxSizeMB = round( $maxSize / 1048576, 2 );
 			$this->_errors[] = "File size exceeds maximum allowed size of {$maxSizeMB}MB";
 			return false;
 		}

-		if( $size === 0 )
-		{
-			$this->_errors[] = 'File is empty';
-			return false;
-		}
-
 		return true;
 	}
tests/Cms/Services/Media/MediaValidatorTest.php (1)

163-167: Consider adding a test for valid image upload and MIME type mismatch.

The comment acknowledges that full validation with real image files is complex, but you could use a minimal valid JPEG/PNG byte sequence to test the happy path. Additionally, testing a file with a valid extension but invalid MIME type (e.g., a .jpg file containing text) would improve coverage.

Would you like me to provide example code for creating a minimal valid image in vfsStream for the happy path test?

src/Cms/Controllers/Admin/Media.php (2)

133-144: Using exit makes the controller difficult to unit test.

The exit calls prevent proper testing and don't align well with MVC patterns. Consider returning early or throwing an exception that the framework catches, or using a response object pattern.

If refactoring is not feasible now, you could at least extract the exit to a protected method that can be mocked in tests:

+	/**
+	 * Terminate the response (can be overridden in tests)
+	 */
+	protected function terminate(): void
+	{
+		exit;
+	}
+
 	private function returnEditorJsSuccess( array $result ): void
 	{
 		echo json_encode( [
 			'success' => 1,
 			'file' => [
 				'url' => $result['url'],
 				'width' => $result['width'],
 				'height' => $result['height']
 			]
 		] );
-		exit;
+		$this->terminate();
 	}

Also applies to: 168-175


49-82: Consider extracting common upload logic to reduce duplication.

Both uploadImage and uploadFeaturedImage share the same validation and upload logic, differing only in response format. This could be refactored to a private helper.

private function processUpload(): array
{
    if( !isset( $_FILES['image'] ) )
    {
        throw new \InvalidArgumentException( 'No file was uploaded' );
    }

    $file = $_FILES['image'];

    if( !$this->_validator->validate( $file ) )
    {
        throw new \InvalidArgumentException( $this->_validator->getFirstError() );
    }

    return $this->_uploader->upload( $file['tmp_name'] );
}

Also applies to: 92-125

src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)

424-437: runMigrations() is misleading - it doesn't run migrations.

The method name suggests it runs migrations, but it only outputs instructions. Either rename it to displayMigrationInstructions() or implement actual migration execution.

-	private function runMigrations(): bool
+	private function displayMigrationInstructions(): void
 	{
-		$this->output->writeln( "Running migrations...\n" );
-
-		// For now, instruct user to run migrations manually
-		// In future, could integrate with MigrationManager
-
 		$this->output->info( "Run: php neuron db:migrate" );
-
-		return true;
 	}

Update the call site at line 145 accordingly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ebd824 and 0e0d43e.

📒 Files selected for processing (38)
  • .github/workflows/ci.yml (1 hunks)
  • .gitignore (1 hunks)
  • MIGRATIONS.md (1 hunks)
  • UPGRADE_NOTES.md (1 hunks)
  • composer.json (2 hunks)
  • readme.md (1 hunks)
  • resources/.cms-manifest.json (1 hunks)
  • resources/config/database.yaml.example (0 hunks)
  • resources/config/email.yaml.example (0 hunks)
  • resources/config/maintenance.yaml.example (0 hunks)
  • resources/config/neuron.yaml.example (1 hunks)
  • resources/config/routes.yaml (1 hunks)
  • resources/database/migrate/20250111000000_create_users_table.php (1 hunks)
  • resources/database/migrate/20251205000000_add_two_factor_and_timezone_to_users.php (1 hunks)
  • resources/views/admin/posts/create.php (3 hunks)
  • resources/views/admin/posts/edit.php (3 hunks)
  • src/Cms/Cli/Commands/Install/InstallCommand.php (2 hunks)
  • src/Cms/Cli/Commands/Install/UpgradeCommand.php (1 hunks)
  • src/Cms/Cli/Provider.php (1 hunks)
  • src/Cms/Controllers/Admin/Media.php (1 hunks)
  • src/Cms/Dtos/MediaUploadDto.yaml (1 hunks)
  • src/Cms/Models/Post.php (5 hunks)
  • src/Cms/Repositories/DatabaseCategoryRepository.php (7 hunks)
  • src/Cms/Repositories/DatabasePageRepository.php (5 hunks)
  • src/Cms/Repositories/DatabasePostRepository.php (13 hunks)
  • src/Cms/Repositories/DatabaseTagRepository.php (6 hunks)
  • src/Cms/Repositories/DatabaseUserRepository.php (5 hunks)
  • src/Cms/Services/Media/CloudinaryUploader.php (1 hunks)
  • src/Cms/Services/Media/IMediaUploader.php (1 hunks)
  • src/Cms/Services/Media/MediaValidator.php (1 hunks)
  • tests/Cms/BlogControllerTest.php (3 hunks)
  • tests/Cms/Repositories/DatabaseCategoryRepositoryTest.php (2 hunks)
  • tests/Cms/Repositories/DatabasePostRepositoryTest.php (3 hunks)
  • tests/Cms/Repositories/DatabaseTagRepositoryTest.php (2 hunks)
  • tests/Cms/Repositories/DatabaseUserRepositoryTest.php (3 hunks)
  • tests/Cms/Services/AuthenticationTest.php (3 hunks)
  • tests/Cms/Services/Media/CloudinaryUploaderTest.php (1 hunks)
  • tests/Cms/Services/Media/MediaValidatorTest.php (1 hunks)
💤 Files with no reviewable changes (3)
  • resources/config/maintenance.yaml.example
  • resources/config/email.yaml.example
  • resources/config/database.yaml.example
✅ Files skipped from review due to trivial changes (2)
  • resources/.cms-manifest.json
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • composer.json
🧰 Additional context used
🧬 Code graph analysis (13)
resources/views/admin/posts/edit.php (2)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Models/Post.php (1)
  • getContentRaw (133-136)
src/Cms/Services/Media/IMediaUploader.php (2)
src/Cms/Controllers/Admin/Media.php (1)
  • Media (18-192)
src/Cms/Services/Media/CloudinaryUploader.php (3)
  • upload (67-88)
  • uploadFromUrl (98-120)
  • delete (129-142)
src/Cms/Services/Media/MediaValidator.php (2)
src/Cms/Controllers/Admin/Media.php (2)
  • Media (18-192)
  • __construct (26-39)
src/Cms/Services/Media/CloudinaryUploader.php (1)
  • __construct (27-31)
src/Cms/Controllers/Admin/Media.php (3)
src/Cms/Services/Media/CloudinaryUploader.php (3)
  • CloudinaryUploader (16-197)
  • __construct (27-31)
  • upload (67-88)
src/Cms/Services/Media/MediaValidator.php (3)
  • MediaValidator (14-192)
  • __construct (24-27)
  • getFirstError (188-191)
src/Cms/Services/Media/IMediaUploader.php (1)
  • upload (22-22)
tests/Cms/Services/Media/CloudinaryUploaderTest.php (2)
src/Cms/Services/Media/CloudinaryUploader.php (3)
  • CloudinaryUploader (16-197)
  • upload (67-88)
  • uploadFromUrl (98-120)
src/Cms/Services/Media/IMediaUploader.php (2)
  • upload (22-22)
  • uploadFromUrl (32-32)
src/Cms/Repositories/DatabasePostRepository.php (5)
src/Cms/Models/Post.php (9)
  • fromArray (488-585)
  • getId (57-60)
  • setCategories (376-380)
  • setTags (436-440)
  • toArray (592-609)
  • setId (65-69)
  • getCategories (366-369)
  • getTags (426-429)
  • incrementViewCount (321-325)
src/Cms/Models/Category.php (4)
  • fromArray (143-175)
  • getId (37-40)
  • toArray (182-192)
  • setId (45-49)
src/Cms/Models/Tag.php (4)
  • fromArray (125-156)
  • getId (36-39)
  • toArray (163-172)
  • setId (44-48)
src/Cms/Models/User.php (4)
  • fromArray (438-489)
  • getId (61-64)
  • toArray (413-433)
  • setId (69-73)
src/Cms/Repositories/IPostRepository.php (9)
  • findBySlug (22-22)
  • create (27-27)
  • count (104-104)
  • delete (37-37)
  • all (47-47)
  • getByAuthor (56-56)
  • getByCategory (65-65)
  • getByTag (74-74)
  • incrementViewCount (109-109)
src/Cms/Services/Media/CloudinaryUploader.php (3)
src/Cms/Controllers/Admin/Media.php (2)
  • Media (18-192)
  • __construct (26-39)
src/Cms/Services/Media/MediaValidator.php (1)
  • __construct (24-27)
src/Cms/Services/Media/IMediaUploader.php (3)
  • upload (22-22)
  • uploadFromUrl (32-32)
  • delete (41-41)
src/Cms/Repositories/DatabaseUserRepository.php (4)
src/Cms/Repositories/DatabaseCategoryRepository.php (4)
  • create (77-98)
  • delete (131-137)
  • all (142-145)
  • count (150-153)
src/Cms/Repositories/DatabasePostRepository.php (4)
  • create (115-144)
  • delete (212-218)
  • all (223-240)
  • count (338-348)
src/Cms/Repositories/DatabaseTagRepository.php (4)
  • create (61-82)
  • delete (115-121)
  • all (126-129)
  • count (134-137)
src/Cms/Models/User.php (3)
  • toArray (413-433)
  • setId (69-73)
  • getId (61-64)
src/Cms/Repositories/DatabaseCategoryRepository.php (2)
src/Cms/Repositories/DatabaseTagRepository.php (4)
  • create (61-82)
  • all (126-129)
  • count (134-137)
  • allWithPostCount (142-163)
src/Cms/Models/Category.php (3)
  • toArray (182-192)
  • setId (45-49)
  • getId (37-40)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)
src/Cms/Cli/Commands/Install/InstallCommand.php (2)
  • __construct (25-32)
  • getName (37-40)
tests/Cms/Services/Media/MediaValidatorTest.php (1)
src/Cms/Services/Media/MediaValidator.php (4)
  • MediaValidator (14-192)
  • validate (35-73)
  • getErrors (178-181)
  • getFirstError (188-191)
src/Cms/Repositories/DatabaseTagRepository.php (1)
src/Cms/Models/Tag.php (3)
  • toArray (163-172)
  • setId (44-48)
  • getId (36-39)
resources/views/admin/posts/create.php (1)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
🪛 actionlint (1.7.9)
.github/workflows/ci.yml

27-27: the runner of "codecov/codecov-action@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 LanguageTool
UPGRADE_NOTES.md

[style] ~44-~44: The double modal “Required Run” is nonstandard (only accepted in certain dialects). Consider “to be Run”.
Context: ... Changes - None ### Action Required 1. Run php neuron cms:upgrade to copy new mi...

(NEEDS_FIXED)

🪛 PHPMD (2.15.0)
src/Cms/Cli/Commands/Install/UpgradeCommand.php

61-61: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)


406-406: Avoid unused local variables such as '$installedMigrations'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (55)
src/Cms/Cli/Commands/Install/InstallCommand.php (2)

9-10: Namespace migration verified as complete.

No references to the old Neuron\Data\Setting\... namespace remain in the codebase. The migration to Neuron\Data\Settings\... is consistently applied across all importing files, including multiple repositories, services, and tests. The imports at lines 9-10 are correct and aligned with the rest of the codebase.


185-186: The upload directories are unused; files are uploaded to Cloudinary, not stored locally.

The /storage/uploads and /storage/uploads/temp directories are created during installation but are never used. The actual upload flow validates files via MediaValidator (which checks MIME type, extension, and image integrity) and then uploads directly to Cloudinary. No local file storage, caching, or temporary handling occurs.

The security concerns and cleanup mechanisms mentioned in the original review are not applicable since files don't reside on disk. If these directories are intended for future local storage fallback or caching, that capability should be implemented and explicitly documented.

Likely an incorrect or invalid review comment.

resources/config/neuron.yaml.example (1)

102-108: Migrations config: ensure paths/table name match the migration tool’s expectations

The migrations block is sensible and clearly documented. Correctness depends on how your migration tooling is wired.

  • Verify that path, seeds_path, and schema_file are resolved relative to project root in the same way the migration runner expects.
  • Confirm that table: phinx_log is consistent with how your migration library is configured elsewhere (many Phinx setups use phinxlog), or that you’ve intentionally standardized on this custom name.
  • Make sure auto_dump_schema is wired into the migration command so toggling it here has the intended effect.
readme.md (1)

2-2: LGTM!

The Codecov badge addition aligns well with the CI workflow changes that now upload coverage reports.

.github/workflows/ci.yml (1)

13-24: LGTM!

The PHP setup with pcov for coverage and the PHPUnit configuration are well-structured. Using --coverage-filter src ensures only source code is measured.

src/Cms/Repositories/DatabaseCategoryRepository.php (8)

37-40: LGTM!

Clean ORM usage for the findById method.


45-56: LGTM!

The findBySlug and findByName methods correctly use the ORM's fluent query builder pattern.


64-72: LGTM!

Good defensive check for empty array before querying, and proper use of whereIn for batch lookups.


103-126: LGTM!

The update method properly handles duplicate detection while excluding the current record, and uses the ORM's save() method correctly.


131-137: LGTM!

The delete method correctly uses the query builder and returns a meaningful boolean result.


142-153: LGTM!

The all() and count() methods are concise and leverage the ORM effectively.


158-179: LGTM!

The raw SQL approach for the JOIN + aggregation query is consistent with DatabaseTagRepository::allWithPostCount(). The TODO comment appropriately documents the ORM limitation.


77-98: The code is correct as-is. While toArray() does return dates as 'Y-m-d H:i:s' strings, the Category model's fromArray() method explicitly handles string-to-DateTimeImmutable conversion (lines 142-158), which the ORM's create() method uses internally. Tests confirm this works correctly without any changes needed.

src/Cms/Repositories/DatabasePageRepository.php (1)

32-36: LGTM on eager loading for author.

Using Page::with('author') for eager loading is the correct approach to avoid N+1 queries when accessing author data.

src/Cms/Repositories/DatabaseUserRepository.php (2)

133-136: ORM query pattern looks correct.

The User::orderBy('created_at', 'DESC')->all() pattern is consistent with other repositories and provides expected ordering.


83-88: No action needed; password hashing is handled correctly.

The password is already hashed in the service layer (via PasswordHasher) before being set on the User model through setPasswordHash(). The User model's setPasswordHash() method (line 120-124) simply stores the hash value without modification. When User::create($user->toArray()) is called, it persists the pre-computed hash as-is; there is no re-hashing mechanism in the model or ORM layer.

src/Cms/Repositories/DatabaseTagRepository.php (3)

142-163: Raw SQL for allWithPostCount() is appropriate.

Keeping raw SQL for the JOIN with aggregation is the right pragmatic choice. The TODO comment clearly indicates future work.


108-109: Ensure save() is called on an ORM-hydrated instance.

If $tag was instantiated via new Tag() rather than fetched from the database, save() may not work correctly depending on ORM implementation. Verify this path is only reached with ORM-loaded entities.


75-79: This pattern is standard and working across all models in the codebase.

The concern about passing null id in toArray() to create() is unfounded. The exact same pattern (Model::create($entity->toArray())) is used consistently across all repositories (User, Post, Page, Category, Tag) and is functioning properly. The Neuron ORM (v0.1.*) handles null values in mass assignment gracefully, treating null values as absent fields that don't override database defaults like auto-increment IDs. No special filtering is needed.

Likely an incorrect or invalid review comment.

src/Cms/Repositories/DatabasePostRepository.php (2)

163-196: Raw SQL UPDATE with ORM CREATE is intentional but worth documenting.

The comment on lines 163-164 explains why raw SQL is used for updates. This asymmetry (ORM for create, raw SQL for update) is acceptable given the ORM limitation, but consider adding a TODO to revisit when the ORM supports dirty tracking.


76-110: loadRelations helper is well-structured.

The method cleanly separates loading categories and tags via JOINs. Good use of array_map with arrow functions.

resources/database/migrate/20251205000000_add_two_factor_and_timezone_to_users.php (1)

26-43: The after column references are safe—remove unnecessary idempotency checks.

The columns two_factor_secret and last_login_at are created in the initial migration, so the after placement is valid. However, two_factor_recovery_codes and timezone are already defined in 20250111000000_create_users_table.php. The hasColumn() guards make this migration safe for existing installations, but the column definitions are redundant for new installs. Either consolidate these columns into the initial migration, or clarify in code comments why this migration exists as a separate patch.

resources/database/migrate/20250111000000_create_users_table.php (1)

25-25: LGTM! Verify recovery codes are encrypted at the application layer.

The two_factor_recovery_codes column is appropriately defined as nullable TEXT. Since recovery codes are sensitive authentication credentials, ensure they are encrypted before storage at the application layer.

tests/Cms/Repositories/DatabaseTagRepositoryTest.php (1)

8-8: LGTM! ORM initialization follows consistent test pattern.

The ORM initialization with Model::setPdo() in setUp is consistent with the broader migration to ORM-based data access across repository tests.

Also applies to: 33-34

tests/Cms/Repositories/DatabaseCategoryRepositoryTest.php (1)

8-8: LGTM! ORM initialization follows consistent test pattern.

The ORM initialization matches the pattern used in other repository tests, supporting the migration to ORM-based data access.

Also applies to: 33-34

tests/Cms/Repositories/DatabaseUserRepositoryTest.php (1)

8-9: LGTM! Test updates align with ORM migration and 2FA support.

The changes correctly reflect:

  • Namespace migration for SettingManager
  • ORM initialization consistent with other repository tests
  • Schema update for two_factor_recovery_codes matching the migration

Also applies to: 47-48, 66-66

src/Cms/Cli/Provider.php (1)

21-30: LGTM! Upgrade command registration follows CLI conventions.

The new cms:upgrade command is properly registered using the same pattern as existing commands. The comment update to plural form is appropriate.

src/Cms/Dtos/MediaUploadDto.yaml (1)

1-19: LGTM! Validation rules are appropriate for media uploads.

The DTO configuration correctly defines:

  • Required file field with NotEmpty validation
  • Optional folder field with reasonable 255-character limit
tests/Cms/BlogControllerTest.php (3)

13-16: LGTM! ORM initialization and Settings migration applied correctly.

The test setup now properly initializes the ORM and uses the migrated Settings namespace, consistent with other test files.

Also applies to: 54-55


100-120: LGTM! Users table creation supports blog author functionality.

The addition of the users table in test setup is appropriate if the Blog controller needs to access author information. The schema correctly includes the new 2FA fields.


129-129: LGTM! Editor.js default structure is correctly formatted.

The content_raw column default value '{"blocks":[]}' is a valid empty Editor.js document structure, supporting the block-based editor upgrade.

tests/Cms/Services/Media/CloudinaryUploaderTest.php (3)

17-29: LGTM! Test setup correctly initializes Cloudinary configuration.

The in-memory settings with test Cloudinary credentials are appropriate for unit testing the uploader service.


31-61: LGTM! Unit tests appropriately cover error paths.

The tests correctly validate exception handling for:

  • Missing configuration
  • Non-existent files
  • Invalid URLs

68-113: LGTM! Integration tests appropriately marked incomplete.

Marking integration tests as incomplete is correct since they require actual Cloudinary credentials and external resources. The commented examples provide clear guidance for future integration testing.

UPGRADE_NOTES.md (1)

1-151: Well-structured upgrade documentation.

The upgrade notes provide clear instructions for the upgrade workflow, version-specific changes, and troubleshooting guidance. The section headers and formatting are appropriate for developer documentation.

One consideration: ensure the version numbers (2025.12.5, 2025.11.7) align with actual release tags in the repository to avoid confusion.

tests/Cms/Repositories/DatabasePostRepositoryTest.php (2)

35-36: LGTM - ORM initialization in test setup.

Initializing the ORM with the test PDO connection ensures the repository tests can properly exercise ORM-backed data access.


56-85: Schema updates align with migration requirements.

The test schema correctly includes the new two_factor_recovery_codes, timezone columns for users, and content_raw for posts. The default '{"blocks":[]}' matches the Editor.js empty content structure used in the Post model.

resources/views/admin/posts/edit.php (2)

93-103: Good XSS prevention in content loading.

Using JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT flags ensures the JSON output is safe for embedding in a script context, preventing injection attacks via malicious post content.


199-211: Form submission pattern is correct.

Calling e.target.submit() after e.preventDefault() correctly bypasses the event listener and performs a native form submission, avoiding an infinite loop.

tests/Cms/Services/AuthenticationTest.php (2)

11-12: LGTM - Updated imports for ORM and Settings namespace.

The namespace migration from Neuron\Data\Setting to Neuron\Data\Settings and the addition of ORM initialization align with the broader refactoring in this PR.


50-51: ORM initialization added correctly.

Initializing the ORM with the test PDO connection before creating tables ensures proper ORM functionality during authentication tests.

resources/views/admin/posts/create.php (1)

75-82: CDN versions are now pinned - previous feedback addressed.

The script versions are pinned to specific versions (e.g., @2.29.1, @2.8.1), which addresses the previous review concern about using @latest.

src/Cms/Services/Media/IMediaUploader.php (1)

12-42: Well-designed media uploader interface.

The IMediaUploader interface provides a clean contract for media operations with appropriate PHPDoc documentation. The return array structure (url, public_id, width, height, format) is well-documented and aligns with the CloudinaryUploader implementation shown in the relevant snippets.

src/Cms/Models/Post.php (2)

21-22: Good design for dual content storage.

Maintaining both _body (plain text) and _contentRaw (Editor.js JSON) provides backward compatibility for code that expects plain text while supporting the new block-based editor format.


499-533: Content initialization in fromArray is well-structured.

The logic correctly handles multiple input formats (content_raw as string/array, content as string/array) and properly falls back to extracting plain text when body is not explicitly provided.

src/Cms/Services/Media/MediaValidator.php (1)

14-73: LGTM!

The overall validation flow is well-structured with proper early returns, error accumulation, and comprehensive checks including upload errors, file existence, size, and type validation. The use of is_uploaded_file() could provide additional security for production environments, but file_exists() is acceptable for general validation.

tests/Cms/Services/Media/MediaValidatorTest.php (2)

94-111: Test relies on $file['size'] from array, not actual file size.

The test creates a file with 'fake content' (12 bytes) but passes size => 10485760 in the array. This works because MediaValidator trusts $file['size'] from the $_FILES superglobal. This is correct behavior for testing the validator, but consider adding a comment clarifying that PHP populates $_FILES['size'] at upload time.


20-34: LGTM!

Good test setup with virtual filesystem and in-memory settings. The configuration matches the validator's expected defaults, enabling consistent test behavior.

src/Cms/Controllers/Admin/Media.php (1)

26-39: LGTM!

Good defensive check for Settings in the constructor with a clear exception message. The dependency injection via Registry is appropriate for this framework.

src/Cms/Services/Media/CloudinaryUploader.php (3)

39-57: LGTM!

Good validation of required Cloudinary configuration with a clear error message. The exception during construction ensures fast-fail behavior.


184-196: LGTM!

Clean normalization of Cloudinary response with sensible defaults for missing fields. Preferring secure_url over url is the correct approach.


67-88: LGTM!

Good file existence validation before upload attempt, and proper exception chaining preserves the original error context.

src/Cms/Cli/Commands/Install/UpgradeCommand.php (3)

61-61: Unused $parameters is acceptable for interface compliance.

The static analysis flags $parameters as unused, but this is a common pattern for CLI command implementations where the method signature is dictated by the parent class. No action needed.


21-28: LGTM!

Constructor follows the same pattern as InstallCommand, using getcwd() for project path and relative path traversal for component path.


284-290: LGTM!

Clean use of array_diff to identify new migrations by comparing package and installed lists.

@codecov
Copy link

codecov bot commented Dec 8, 2025

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

26-33: Reconsider fail_ci_if_error: true for codecov upload.

The codecov-action has been updated to v5 (addressing the prior review concern), but the fail_ci_if_error setting differs from the previously suggested value. Setting this to true means your CI will fail if the codecov service experiences outages, network issues, or slowdowns—external factors outside your control.

Industry best practice is to set this to false to prevent codecov availability issues from blocking merges. Consider reverting unless you have a specific requirement for codecov success to be mandatory.

Apply this diff to align with best practices:

       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: ./coverage.xml
           flags: cms
           slug: Neuron-PHP/cms
-          fail_ci_if_error: true
+          fail_ci_if_error: false
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0d43e and ab623e7.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml (1 hunks)
  • readme.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • readme.md
🔇 Additional comments (1)
.github/workflows/ci.yml (1)

13-24: Modern CI setup with solid coverage configuration.

The workflow updates to use shivammathur/setup-php@v2 with PHP 8.4 is a clean improvement over container-based execution. The Composer install flags (--optimize-autoloader, --no-interaction) and PHPUnit coverage command (with --coverage-clover and --coverage-filter src) are both well-configured and follow CI best practices.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
tests/Cms/Services/CsrfTokenTest.php (2)

59-73: Test name vs. implementation mismatch.

The test name testGenerateTokenIsDifferentEachTime suggests verifying that calling generate() multiple times on the same instance produces different tokens. However, the test creates two separate CsrfToken instances with different seeds. This effectively tests that different seeds produce different tokens, not that successive calls produce different tokens.

Consider renaming to testDifferentInstancesWithDifferentSeedsProduceDifferentTokens or restructuring to test multiple generate() calls on a single instance (if FakeRandom supports advancing state per call).


179-186: Consider tightening the regex to match hex format.

According to the CsrfToken implementation, tokens are generated using $this->random->string(64, 'hex'), which produces lowercase hexadecimal characters (0-9, a-f). The current regex /^[a-zA-Z0-9]+$/ is overly permissive.

-		$this->assertMatchesRegularExpression('/^[a-zA-Z0-9]+$/', $token);
+		$this->assertMatchesRegularExpression('/^[a-f0-9]+$/', $token);
src/Cms/Services/Auth/CsrfToken.php (2)

6-7: Solid randomness DI; consider minor consistency & DI refinements

Injecting IRandom with a RealRandom default keeps existing call sites working and makes the class testable, which is great. Two small follow‑ups you might consider:

  • Align the property name with the existing private member convention ($_sessionManager, $_tokenKey) to keep style consistent (e.g., private IRandom $_random; and $this->_random).
  • Instead of new RealRandom() inside the constructor, you could let your DI container/wiring provide the default implementation, which keeps this service fully configuration-driven and avoids hard-coding a concrete in the service itself.

These are non-blocking and can be deferred.

Also applies to: 21-27


34-34: Confirm RealRandom::string() is CSPRNG-backed for CSRF tokens

Previously this used random_bytes, which is explicitly CSPRNG-backed. CSRF tokens must stay cryptographically unpredictable, so please verify that RealRandom::string(64, 'hex') is implemented on top of a cryptographically secure source and yields comparable entropy (effectively 64 hex chars ≈ 256 bits). If IRandom can be backed by weaker RNGs, consider tightening its contract or introducing a dedicated crypto-safe interface for security-sensitive use like CSRF.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab623e7 and 6157f19.

📒 Files selected for processing (2)
  • src/Cms/Services/Auth/CsrfToken.php (2 hunks)
  • tests/Cms/Services/CsrfTokenTest.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/Cms/Services/Auth/CsrfToken.php (2)
src/Cms/Controllers/Member/Registration.php (1)
  • __construct (40-61)
src/Cms/Controllers/Auth/Login.php (1)
  • __construct (30-44)
tests/Cms/Services/CsrfTokenTest.php (2)
src/Cms/Auth/SessionManager.php (1)
  • SessionManager (13-188)
src/Cms/Services/Auth/CsrfToken.php (5)
  • CsrfToken (17-75)
  • generate (32-37)
  • regenerate (71-74)
  • getToken (42-50)
  • validate (55-66)
🔇 Additional comments (3)
tests/Cms/Services/CsrfTokenTest.php (3)

20-34: LGTM! Good use of dependency injection for deterministic testing.

The setUp properly injects FakeRandom into CsrfToken, enabling reproducible token generation in tests. This is a solid pattern for testing randomness-dependent code.


142-157: LGTM! Regeneration behavior is properly tested.

The test correctly verifies that regenerate() produces a different token and updates the stored token. The seed manipulation approach effectively simulates the randomness that would occur in production.


159-177: LGTM! Token invalidation is properly verified.

This test correctly ensures that after regeneration, the old token becomes invalid while the new token is accepted. Good coverage for security-critical CSRF behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/Cms/Services/Auth/PasswordResetter.php (1)

112-116: Event emissions are well‑placed; consider decoupling IP/source retrieval as a small cleanup

Emitting PasswordResetRequestedEvent and PasswordResetCompletedEvent after the respective operations, with User and $_SERVER['REMOTE_ADDR'] ?? 'unknown', is sensible and should give useful audit signals.

If you want to tighten design further, consider:

  • Centralizing IP acquisition (e.g., a small helper or a request/context service) instead of reading $_SERVER directly in multiple places.
  • Optionally routing events via an injected dispatcher instead of Event::emit for easier testing/mocking.

These are architectural nits; current code is fine to ship.

Also applies to: 181-185

src/Cms/Services/Auth/EmailVerifier.php (2)

28-57: Random generator DI is solid; consider minor naming consistency

Injecting ?IRandom $random and defaulting to new RealRandom() mirrors the pattern used in CsrfToken and PasswordResetter, improving testability while preserving secure defaults. The only minor nit is that this class uses $_random whereas others use $this->random; if you care about cross-service consistency/grepability, you might consider aligning the property name, but it’s not critical.


169-172: Emitting EmailVerifiedEvent is good; consider isolating failures and importing classes

Emitting EmailVerifiedEvent on successful verification is a nice integration point with the event bus. Two optional improvements:

  • If Event::emit() can throw or otherwise fail, consider wrapping this call in a try/catch and logging failures so that a broken listener doesn’t cause verifyEmail() to surface as an error after the user has already been activated.
  • For readability, you could add use Neuron\Application\CrossCutting\Event; and use Neuron\Cms\Events\EmailVerifiedEvent; at the top and call Event::emit(new EmailVerifiedEvent($user)); instead of using fully-qualified names inline.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6157f19 and 950d410.

📒 Files selected for processing (3)
  • src/Cms/Services/Auth/EmailVerifier.php (5 hunks)
  • src/Cms/Services/Auth/PasswordResetter.php (7 hunks)
  • versionlog.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • versionlog.md
🧰 Additional context used
🧬 Code graph analysis (2)
src/Cms/Services/Auth/EmailVerifier.php (3)
src/Cms/Services/Auth/PasswordResetter.php (1)
  • __construct (45-62)
src/Cms/Services/Auth/CsrfToken.php (1)
  • __construct (23-27)
src/Cms/Events/EmailVerifiedEvent.php (1)
  • EmailVerifiedEvent (24-39)
src/Cms/Services/Auth/PasswordResetter.php (3)
src/Cms/Services/Auth/CsrfToken.php (1)
  • __construct (23-27)
src/Cms/Events/PasswordResetRequestedEvent.php (1)
  • PasswordResetRequestedEvent (24-41)
src/Cms/Events/PasswordResetCompletedEvent.php (1)
  • PasswordResetCompletedEvent (24-41)
🔇 Additional comments (3)
src/Cms/Services/Auth/PasswordResetter.php (1)

10-12: Randomness DI + token generation look correct and improve testability

Injecting IRandom with a RealRandom fallback keeps the constructor backward‑compatible (new optional last parameter), aligns with the CsrfToken pattern, and makes token generation testable. The switch to $this->_random->string(64, 'hex') still yields a 64‑char token which is then SHA‑256 hashed, so stored token shape remains consistent with the previous implementation.

No changes needed from my side here.

Also applies to: 29-29, 43-43, 51-52, 61-61, 96-97

src/Cms/Services/Auth/EmailVerifier.php (2)

10-12: IRandom/RealRandom/SettingManager imports look correct and consistent

Importing Neuron\Core\System\IRandom/RealRandom alongside Neuron\Data\Settings\SettingManager keeps this service aligned with the other auth components using the same abstractions and supports the new DI-based randomness pattern. No changes needed here.


83-86: Token generation remains strong; verify that RealRandom (from neuron-php/mvc) is backed by a cryptographically secure source

The change to use $this->_random->string(64, 'hex') maintains strong security (64-char token = 32 bytes of entropy) and makes randomness pluggable for tests via the IRandom interface. The main consideration is that RealRandom from the external neuron-php/mvc package must remain backed by a cryptographically secure source (e.g., random_bytes() or /dev/urandom); otherwise, email-verification tokens could become guessable if an accidental implementation uses weak randomness in production. Verify RealRandom's implementation in the neuron-php/mvc package to confirm it uses a cryptographically secure random source.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/Cms/Services/Page/Creator.php (1)

21-27: Slug fallback now depends on IRandom::uniqueId being slug‑safe

The fallback 'page-' . $this->_random->uniqueId() assumes uniqueId() only yields characters allowed by your slug rules. If any custom IRandom implementation ever returns characters outside [a-z0-9-], you could end up with invalid slugs since the normalization isn’t re-applied after the fallback.

Either:

  • Document/guarantee on IRandom::uniqueId() that its output is slug-safe, or
  • Re-run the normalization on the fallback string (and mirror that change in the other generateSlug() helpers using this pattern).

Also applies to: 85-99

versionlog.md (1)

1-13: Consider adding a release date to the 0.8.9 heading

Earlier entries (e.g., 0.8.8 2025-11-16) include a date in the heading. Adding the date for 0.8.9 would keep the changelog format consistent and make it easier to track releases over time.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 950d410 and 1ac2d9b.

📒 Files selected for processing (7)
  • src/Cms/Services/Category/Creator.php (4 hunks)
  • src/Cms/Services/Category/Updater.php (4 hunks)
  • src/Cms/Services/Page/Creator.php (4 hunks)
  • src/Cms/Services/Post/Creator.php (6 hunks)
  • src/Cms/Services/Post/Updater.php (6 hunks)
  • src/Cms/Services/Tag/Creator.php (4 hunks)
  • versionlog.md (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/Cms/Services/Tag/Creator.php (3)
src/Cms/Services/Category/Creator.php (1)
  • __construct (25-29)
src/Cms/Services/Page/Creator.php (1)
  • __construct (23-27)
src/Cms/Services/Post/Creator.php (1)
  • __construct (27-38)
src/Cms/Services/Category/Creator.php (4)
src/Cms/Services/Category/Updater.php (1)
  • __construct (25-29)
src/Cms/Services/Page/Creator.php (1)
  • __construct (23-27)
src/Cms/Services/Post/Creator.php (1)
  • __construct (27-38)
src/Cms/Services/Tag/Creator.php (1)
  • __construct (22-26)
src/Cms/Services/Category/Updater.php (6)
src/Cms/Services/Category/Creator.php (1)
  • __construct (25-29)
src/Cms/Services/Page/Creator.php (1)
  • __construct (23-27)
src/Cms/Services/Post/Creator.php (1)
  • __construct (27-38)
src/Cms/Services/Tag/Creator.php (1)
  • __construct (22-26)
src/Cms/Services/Content/ShortcodeParser.php (1)
  • __construct (20-23)
src/Cms/Services/Widget/WidgetRenderer.php (1)
  • __construct (21-24)
src/Cms/Services/Post/Updater.php (5)
src/Cms/Services/Category/Creator.php (1)
  • __construct (25-29)
src/Cms/Services/Page/Creator.php (1)
  • __construct (23-27)
src/Cms/Services/Post/Creator.php (1)
  • __construct (27-38)
src/Cms/Services/Tag/Creator.php (1)
  • __construct (22-26)
src/Cms/Models/Post.php (2)
  • __construct (49-52)
  • setContent (142-147)
src/Cms/Services/Post/Creator.php (5)
src/Cms/Services/Category/Creator.php (1)
  • __construct (25-29)
src/Cms/Services/Page/Creator.php (1)
  • __construct (23-27)
src/Cms/Services/Post/Updater.php (1)
  • __construct (26-37)
src/Cms/Services/Tag/Creator.php (1)
  • __construct (22-26)
src/Cms/Models/Post.php (2)
  • __construct (49-52)
  • setContent (142-147)
🔇 Additional comments (4)
src/Cms/Services/Tag/Creator.php (1)

19-26: Tag creator RNG injection and slug fallback look consistent

The injected random generator and the 'tag-' . uniqueId() fallback align with the other services’ pattern and keep the API of create() unchanged. Nothing concerning here from a correctness or testability standpoint.

Also applies to: 53-67

src/Cms/Services/Post/Updater.php (1)

44-57: Update flow correctly switches to Editor.js content handling

Using string $content (Editor.js JSON) and calling $post->setContent( $content ) matches the Post model’s semantics (updates content_raw and derives the plain-text body), so the update path stays consistent with the new create flow and content model.

Also applies to: 67-88

src/Cms/Services/Category/Creator.php (1)

22-29: Category creator random injection and slug behavior look good

The optional random dependency and the 'category-' . uniqueId() fallback are consistent with the rest of the services and keep create()’s external behavior the same, aside from making slug generation testable. No issues from a correctness perspective.

Also applies to: 79-93

src/Cms/Services/Category/Updater.php (1)

23-29: Updater now mirrors Creator’s slug generation pattern

Injecting the same random source and using the same 'category-' . uniqueId() fallback keeps create/update slug behavior aligned and improves testability. The rest of the update flow is unchanged.

Also applies to: 80-93

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (32)
tests/Unit/Cms/Cli/Commands/Install/InstallCommandTest.php (1)

8-9: Namespace updates look good, but imports appear unused.

The namespace change from Setting to Settings is consistent with the PR's refactoring objectives. However, neither SettingManager nor Yaml appear to be referenced anywhere in this test file—only InstallCommand is actually used.

🔎 Suggested cleanup
 use Neuron\Cms\Cli\Commands\Install\InstallCommand;
 use org\bovigo\vfs\vfsStream;
-use Neuron\Data\Settings\SettingManager;
-use Neuron\Data\Settings\Source\Yaml;
tests/Unit/Cms/Services/AuthenticationTest.php (1)

80-80: Consider adding test coverage for the new column.

The two_factor_recovery_codes column has been added to support 2FA functionality, but there's no test coverage for this field in the authentication tests. While not blocking, consider adding tests to verify that recovery codes are properly stored and retrieved.

tests/Unit/Cms/Repositories/DatabaseUserRepositoryTest.php (1)

66-66: Consider adding test coverage for the new column.

The two_factor_recovery_codes column supports 2FA recovery functionality but lacks test coverage. Consider adding tests to verify the storage and retrieval of recovery codes.

tests/Unit/Cms/Repositories/DatabasePostRepositoryTest.php (2)

56-76: Consider whether the users table is necessary.

A full users table is created in the test setup but never populated or directly used. The tests reference author IDs (1, 2) without creating corresponding user records. While this works for testing post repository operations in isolation, consider whether this table adds unnecessary complexity to the test setup.


85-85: Consider adding test coverage for the new content_raw column.

The content_raw column has been added with a JSON default value to support Editor.js content, but there's no test coverage for this field. Consider adding tests to verify that Editor.js content is properly stored and retrieved in this format.

tests/Unit/Cms/Services/Email/SenderTest.php (5)

19-19: Consider more robust path construction.

The relative path traversal (__DIR__ . '/../../../../..') is fragile and will break if the test file is moved. Consider using a constant defined at the project root or dirname(__FILE__, 6) for clarity.


26-93: Consider verifying internal state, not just return values.

The fluent interface tests only assert that methods return $this, but they don't verify that the data (email addresses, subject, body, attachments) is actually stored correctly. While testing the fluent interface is valuable, these tests could pass even if the methods silently fail to store data.

Consider adding assertions that verify the data was stored, or add integration tests that check the final state before send() is called.


95-125: Verify the rendered template content.

The test creates a template with <h1>Hello <?= $name ?></h1> and passes ['name' => 'John'], but it doesn't verify that the body was actually set to the expected rendered output. Consider adding an assertion that checks the rendered content matches the expected result.

🔎 Suggested verification

After calling template(), you could use reflection to verify the private _body property was set correctly, or refactor the Sender class to expose a getter for testing purposes:

 		$result = $this->sender->template( 'email/test-email', [ 'name' => 'John' ] );

 		$this->assertSame( $this->sender, $result, 'Should return self for fluent interface' );
+		
+		// Use reflection to verify the rendered content
+		$reflection = new \ReflectionClass( $this->sender );
+		$bodyProperty = $reflection->getProperty( '_body' );
+		$bodyProperty->setAccessible( true );
+		$body = $bodyProperty->getValue( $this->sender );
+		
+		$this->assertStringContainsString( '<h1>Hello John</h1>', $body, 'Template should be rendered with data' );

120-120: Fragile directory cleanup logic.

The condition count(scandir($templateDir)) === 2 assumes the directory only contains . and .., but this may not hold if other files exist or if the test fails partway through. Consider using a more robust cleanup approach or a dedicated temp directory that can be safely removed regardless of contents.


135-176: Consider verifying that logging actually occurs.

Both test mode tests (testSendInTestModeLogsEmail and testSendWithMultipleRecipients) only assert that send() returns true, but they don't verify that the email data was actually logged as expected. Consider adding assertions or using a log spy/mock to verify that Log::info() was called with the expected messages.

tests/Unit/Cms/Services/Media/MediaValidatorTest.php (2)

36-58: Minor inconsistency in assertion style.

Line 44 uses assertEquals for the exact error message "No file was uploaded", while line 57 uses assertStringContainsString for what appears to be the same error message. Consider using consistent assertion methods for the same error condition.

🔎 Suggested fix for consistency
-		$this->assertStringContainsString( 'No file was uploaded', $this->_validator->getFirstError() );
+		$this->assertEquals( 'No file was uploaded', $this->_validator->getFirstError() );

36-161: Consider adding tests for successful validation and error state clearing.

The test suite comprehensively covers error cases but lacks:

  1. A happy path test where validation succeeds (though the comment at lines 163-168 acknowledges the complexity of creating valid image data in unit tests)
  2. A test verifying that errors are cleared between successive validate() calls

While these additions are optional given the challenges with unit testing image validation, they would strengthen the test coverage.

tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (2)

31-41: Consider testing individual missing configuration values.

The current test verifies behavior when all Cloudinary configuration is missing. Based on the implementation, the constructor should also throw when any individual value (cloud_name, api_key, or api_secret) is missing. Adding separate tests for each missing value would provide more granular coverage.


68-113: Good approach to marking integration tests as incomplete.

The incomplete tests appropriately acknowledge that they require real Cloudinary credentials and network access. The example code comments provide helpful guidance for future integration testing.

One consideration: these integration tests are in the tests/Unit/ directory. You might want to move them to a separate tests/Integration/ directory when implementing them, to maintain clearer separation between unit and integration tests.

tests/Unit/Cms/Repositories/DatabasePageRepositoryTest.php (2)

114-130: LGTM! Good coverage of find-by-ID scenarios.

Both happy path and non-existent record cases are tested appropriately.

Optional: Verify eager loading of author relationship

The repository implementation uses Page::with('author') for eager loading. You could optionally add an assertion to verify the author is loaded:

 public function testFindByIdReturnsPage(): void
 {
   $page = $this->createTestPage();
   
   $found = $this->repository->findById( $page->getId() );
   
   $this->assertNotNull( $found );
   $this->assertEquals( $page->getId(), $found->getId() );
   $this->assertEquals( $page->getTitle(), $found->getTitle() );
+  $this->assertNotNull( $found->getAuthor() ); // Verify eager loading
 }

250-284: LGTM! Good coverage of retrieval and filtering.

The tests properly verify basic retrieval, status filtering, and pagination with limit/offset.

Optional: Verify DESC ordering by created_at

The repository orders results by created_at DESC. You could optionally verify this ordering:

public function testAllOrdersByCreatedAtDesc(): void
{
    // Create pages with slight delays or explicit timestamps
    $page1 = $this->createTestPage(['title' => 'First']);
    $page2 = $this->createTestPage(['title' => 'Second']);
    $page3 = $this->createTestPage(['title' => 'Third']);
    
    $pages = $this->repository->all();
    
    $this->assertEquals('Third', $pages[0]->getTitle());
    $this->assertEquals('Second', $pages[1]->getTitle());
    $this->assertEquals('First', $pages[2]->getTitle());
}
tests/Unit/Cms/Services/Widget/WidgetRendererTest.php (1)

79-90: Consider asserting the render result.

The test correctly verifies that the custom limit is passed to the repository. For completeness, consider adding an assertion on the render result to ensure the method executes successfully.

🔎 Optional enhancement
 	$renderer = new WidgetRenderer( $repository );
-	$renderer->render( 'latest-posts', ['limit' => 10] );
+	$result = $renderer->render( 'latest-posts', ['limit' => 10] );
+	
+	$this->assertStringContainsString( 'latest-posts-widget', $result );
tests/TESTING.md (2)

7-7: Consider adding a language identifier to the fenced code block.

The fenced code block showing the directory structure could include a language identifier (e.g., text or plaintext) for better markdown rendering consistency.

🔎 Proposed fix
-```
+```text
 tests/
 ├── Unit/                    # Unit tests (fast, isolated, use mocks)

237-237: Capitalize "GitHub" for consistency.

The official platform name should be capitalized as "GitHub" rather than "github".

🔎 Proposed fix
-The CI workflow (`.github/workflows/ci.yml`) runs **both unit and integration tests** to ensure comprehensive validation.
+The CI workflow (`.github/workflows/ci.yml`) runs **both unit and integration tests** to ensure comprehensive validation.

Note: The path .github/workflows/ci.yml should remain lowercase as it's a file path reference.

tests/Unit/Cms/Services/Content/EditorJsRendererTest.php (1)

153-194: Consider adding tests for image styling options.

The EditorJsRenderer::renderImage() supports stretched, withBorder, and withBackground options that add CSS classes. Current tests only verify basic image rendering. Consider adding a test case that validates these styling options if they're important for the feature.

🔎 Example test case for image styling options
public function testRenderImageWithStylingOptions(): void
{
    $data = [
        'blocks' => [
            [
                'type' => 'image',
                'data' => [
                    'file' => [ 'url' => 'https://example.com/image.jpg' ],
                    'stretched' => true,
                    'withBorder' => true,
                    'withBackground' => true
                ]
            ]
        ]
    ];

    $result = $this->renderer->render( $data );

    $this->assertStringContainsString( 'w-100', $result );  // stretched
    $this->assertStringContainsString( 'border', $result ); // withBorder
    $this->assertStringContainsString( 'bg-light', $result ); // withBackground
}
tests/Unit/Cms/Services/Page/DeleterTest.php (1)

10-66: Good test coverage for Page Deleter.

Tests cover the essential scenarios: successful deletion, handling pages without IDs, and repository failures. The test structure is clean and follows project conventions.

Note: Unlike Post\Deleter which includes a deleteById() method, Page\Deleter only provides a delete() method that accepts a Page entity. The test suite appropriately covers all available methods.

tests/Unit/Cms/Services/Page/UpdaterTest.php (1)

60-63: Consider adding ->with($page) constraint for consistency.

While this mock setup works correctly, it's less strict than the first test method. For consistency and better verification across all tests, consider adding ->with($page) to the mock expectation.

🔎 Suggested improvement
 		$repository
 			->expects( $this->once() )
 			->method( 'update' )
+			->with( $page )
 			->willReturn( true );
tests/Integration/CategoryTagRelationshipTest.php (1)

25-50: Remove unused variables or utilize them to test parent-child relationships.

The static analysis correctly flags $parentId (line 36) and $childId (line 40) as unused. The docblock mentions "parent category" and "child category", but the code doesn't actually establish or test a parent-child hierarchy—both categories are created as standalone entries.

Either remove these variables if hierarchy testing isn't needed, or add assertions that verify parent-child relationships if that's the intended test scope.

🔎 Proposed fix to remove unused variables
 		$stmt->execute( ['Technology', 'technology', 'Tech articles', $now, $now] );
-		$parentId = (int)$this->pdo->lastInsertId();
+		$this->pdo->lastInsertId(); // Parent category created

 		// Create child category
 		$stmt->execute( ['PHP', 'php', 'PHP programming', $now, $now] );
-		$childId = (int)$this->pdo->lastInsertId();
+		$this->pdo->lastInsertId(); // Child category created
tests/Integration/IntegrationTestCase.php (3)

44-47: Remove empty setUpBeforeClass override.

This method only calls parent::setUpBeforeClass() with no additional logic. It can be safely removed as PHPUnit will call the parent automatically.

🔎 Proposed fix
-	/**
-	 * Set up test database before each test class
-	 */
-	public static function setUpBeforeClass(): void
-	{
-		parent::setUpBeforeClass();
-	}
-

272-284: Consider validating table name to prevent SQL injection.

While this is a protected test helper unlikely to receive untrusted input, the raw string interpolation for the table name is a risky pattern. Consider adding a simple validation to ensure the table name matches expected characters.

🔎 Proposed fix with table name validation
 protected function truncateTable( string $table ): void
 {
+	// Validate table name contains only safe characters
+	if( !preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $table ) )
+	{
+		throw new \InvalidArgumentException( "Invalid table name: {$table}" );
+	}
+
 	$driver = $this->pdo->getAttribute( PDO::ATTR_DRIVER_NAME );

 	if( $driver === 'sqlite' )

232-238: Make $migrationManager nullable for proper null safety.

The property is declared as Manager without ?, but rollbackMigrations checks if it's truthy. In PHP 8+, accessing an uninitialized typed property throws an Error. Either make the type nullable (?Manager) or ensure it's always initialized.

🔎 Proposed fix
 	protected PDO $pdo;
-	protected Manager $migrationManager;
+	protected ?Manager $migrationManager = null;
 	private static bool $migrationsRun = false;
tests/Integration/UserAuthenticationFlowTest.php (1)

259-324: Consider testing the actual lockout enforcement.

The test simulates failed login attempts and account locking, but doesn't verify that a locked account actually prevents login. Consider adding an assertion that queries for the lock status before the successful login reset, to ensure the lock mechanism would work as expected.

Current test verifies data persistence but not the business logic enforcement.

tests/Integration/PageManagementFlowTest.php (2)

109-111: Consider removing sleep(1) in favor of a deterministic approach.

Using sleep(1) to ensure timestamp differences slows down the test suite. Since you're already capturing $now before the insert and $newNow after the sleep, you could instead rely on comparing the actual stored values or manually set different timestamps.

🔎 Proposed fix
 		$pageId = (int)$this->pdo->lastInsertId();
 
 		// Update page
-		sleep(1); // Ensure updated_at is different
+		// Use a slightly different timestamp for the update
 		$updatedContent = json_encode([
 			'blocks' => [
 				['type' => 'paragraph', 'data' => ['text' => 'Updated content']]
 			]
 		]);
 
 		$stmt = $this->pdo->prepare(
 			"UPDATE pages
 			SET title = ?, content = ?, template = ?, meta_title = ?, updated_at = ?
 			WHERE id = ?"
 		);
 
-		$newNow = date( 'Y-m-d H:i:s' );
+		// Add one second to ensure the timestamp is different
+		$newNow = date( 'Y-m-d H:i:s', strtotime( $now ) + 1 );
 		$stmt->execute([

331-337: Consider using prepared statement outside the loop for better efficiency.

The $stmt prepared statement inside the loop on lines 334-336 is re-prepared on each iteration. While this works correctly, preparing it once before the loop would be more efficient.

🔎 Proposed refactor
 		// Simulate 5 page views
+		$updateStmt = $this->pdo->prepare(
+			"UPDATE pages SET view_count = view_count + 1 WHERE id = ?"
+		);
+		$selectStmt = $this->pdo->prepare( "SELECT view_count FROM pages WHERE id = ?" );
+
 		for( $i = 1; $i <= 5; $i++ )
 		{
-			$stmt = $this->pdo->prepare(
-				"UPDATE pages SET view_count = view_count + 1 WHERE id = ?"
-			);
-			$stmt->execute( [$pageId] );
+			$updateStmt->execute( [$pageId] );
 
 			// Verify count incremented
-			$stmt = $this->pdo->prepare( "SELECT view_count FROM pages WHERE id = ?" );
-			$stmt->execute( [$pageId] );
-			$count = (int)$stmt->fetchColumn();
+			$selectStmt->execute( [$pageId] );
+			$count = (int)$selectStmt->fetchColumn();
 
 			$this->assertEquals( $i, $count );
 		}
tests/Integration/TagManagementTest.php (2)

218-226: Remove unused variables $unusedTagId1 and $unusedTagId2.

These variables are captured but never used in assertions. The test already verifies unused tags by name, so the IDs are unnecessary. Based on static analysis hints.

🔎 Proposed fix
 		$stmt->execute( ['used-tag', 'used-tag', $now, $now] );
 		$usedTagId = (int)$this->pdo->lastInsertId();
 
-		$stmt->execute( ['unused-tag-1', 'unused-tag-1', $now, $now] );
-		$unusedTagId1 = (int)$this->pdo->lastInsertId();
-
-		$stmt->execute( ['unused-tag-2', 'unused-tag-2', $now, $now] );
-		$unusedTagId2 = (int)$this->pdo->lastInsertId();
+		$stmt->execute( ['unused-tag-1', 'unused-tag-1', $now, $now] );
+		$stmt->execute( ['unused-tag-2', 'unused-tag-2', $now, $now] );

385-405: Potential test flakiness if tag names have same post_count.

The query orders by post_count DESC but doesn't include a secondary sort. If two tags have the same count, the order could be non-deterministic. In this specific test the counts are unique (15, 10, 5, 2), so it's not an issue now, but adding a secondary sort would make the test more robust.

🔎 Proposed fix for deterministic ordering
 		$stmt = $this->pdo->prepare(
 			"SELECT t.name, COUNT(pt.post_id) as post_count
 			FROM tags t
 			LEFT JOIN post_tags pt ON t.id = pt.tag_id
 			GROUP BY t.id, t.name
-			ORDER BY post_count DESC
+			ORDER BY post_count DESC, t.name ASC
 			LIMIT 3"
 		);
tests/Unit/Cms/Services/CsrfTokenTest.php (1)

179-186: Optional: Regex could be more specific for hex format.

The test correctly validates that tokens are URL-safe alphanumeric characters. However, since the implementation uses 'hex' format (which produces [0-9a-f] characters), the regex could be more precise to match the actual format:

-	$this->assertMatchesRegularExpression('/^[a-zA-Z0-9]+$/', $token);
+	$this->assertMatchesRegularExpression('/^[0-9a-f]+$/', $token);

This is a minor refinement and the current test will pass correctly.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ac2d9b and 8060076.

📒 Files selected for processing (44)
  • .github/workflows/ci.yml (1 hunks)
  • UPGRADE_NOTES.md (1 hunks)
  • resources/database/migrate/20250111000000_create_users_table.php (0 hunks)
  • src/Cms/Auth/Filters/CsrfFilter.php (1 hunks)
  • src/Cms/Controllers/Traits/UsesDtos.php (2 hunks)
  • tests/Integration/CategoryTagRelationshipTest.php (1 hunks)
  • tests/Integration/IntegrationTestCase.php (1 hunks)
  • tests/Integration/PageManagementFlowTest.php (1 hunks)
  • tests/Integration/PostPublishingFlowTest.php (1 hunks)
  • tests/Integration/PostPublishingSchedulingTest.php (1 hunks)
  • tests/Integration/TagManagementTest.php (1 hunks)
  • tests/Integration/UserAuthenticationFlowTest.php (1 hunks)
  • tests/TESTING.md (1 hunks)
  • tests/Unit/Cms/BlogControllerTest.php (3 hunks)
  • tests/Unit/Cms/Cli/Commands/Install/InstallCommandTest.php (1 hunks)
  • tests/Unit/Cms/ContentControllerTest.php (1 hunks)
  • tests/Unit/Cms/Maintenance/MaintenanceConfigTest.php (1 hunks)
  • tests/Unit/Cms/Repositories/DatabaseCategoryRepositoryTest.php (2 hunks)
  • tests/Unit/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (1 hunks)
  • tests/Unit/Cms/Repositories/DatabasePageRepositoryTest.php (1 hunks)
  • tests/Unit/Cms/Repositories/DatabasePasswordResetTokenRepositoryTest.php (1 hunks)
  • tests/Unit/Cms/Repositories/DatabasePostRepositoryTest.php (3 hunks)
  • tests/Unit/Cms/Repositories/DatabaseTagRepositoryTest.php (2 hunks)
  • tests/Unit/Cms/Repositories/DatabaseUserRepositoryTest.php (3 hunks)
  • tests/Unit/Cms/Services/AuthenticationTest.php (3 hunks)
  • tests/Unit/Cms/Services/Content/EditorJsRendererTest.php (1 hunks)
  • tests/Unit/Cms/Services/Content/ShortcodeParserTest.php (1 hunks)
  • tests/Unit/Cms/Services/CsrfTokenTest.php (4 hunks)
  • tests/Unit/Cms/Services/Dto/DtoFactoryServiceTest.php (1 hunks)
  • tests/Unit/Cms/Services/Email/SenderTest.php (1 hunks)
  • tests/Unit/Cms/Services/EmailVerifierTest.php (2 hunks)
  • tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (1 hunks)
  • tests/Unit/Cms/Services/Media/MediaValidatorTest.php (1 hunks)
  • tests/Unit/Cms/Services/Page/CreatorTest.php (1 hunks)
  • tests/Unit/Cms/Services/Page/DeleterTest.php (1 hunks)
  • tests/Unit/Cms/Services/Page/UpdaterTest.php (1 hunks)
  • tests/Unit/Cms/Services/PasswordResetterTest.php (2 hunks)
  • tests/Unit/Cms/Services/Post/CreatorTest.php (9 hunks)
  • tests/Unit/Cms/Services/Post/UpdaterTest.php (10 hunks)
  • tests/Unit/Cms/Services/RegistrationServiceTest.php (1 hunks)
  • tests/Unit/Cms/Services/Widget/WidgetRegistryTest.php (1 hunks)
  • tests/Unit/Cms/Services/Widget/WidgetRendererTest.php (1 hunks)
  • tests/Unit/Cms/Services/Widget/WidgetTest.php (1 hunks)
  • tests/phpunit.xml (1 hunks)
💤 Files with no reviewable changes (1)
  • resources/database/migrate/20250111000000_create_users_table.php
✅ Files skipped from review due to trivial changes (1)
  • tests/Unit/Cms/Services/Content/ShortcodeParserTest.php
🧰 Additional context used
🧬 Code graph analysis (14)
tests/Unit/Cms/Services/Page/DeleterTest.php (3)
tests/Unit/Cms/Services/Category/DeleterTest.php (1)
  • DeleterTest (10-83)
tests/Unit/Cms/Services/Post/DeleterTest.php (1)
  • DeleterTest (10-92)
tests/Unit/Cms/Services/User/DeleterTest.php (1)
  • DeleterTest (10-83)
tests/Unit/Cms/Services/Post/UpdaterTest.php (1)
src/Cms/Models/Post.php (1)
  • getBody (108-111)
tests/Unit/Cms/Services/Media/MediaValidatorTest.php (1)
src/Cms/Services/Media/MediaValidator.php (3)
  • MediaValidator (14-192)
  • getErrors (178-181)
  • getFirstError (188-191)
tests/Unit/Cms/Services/CsrfTokenTest.php (1)
src/Cms/Services/Auth/CsrfToken.php (2)
  • CsrfToken (17-75)
  • generate (32-37)
tests/Unit/Cms/Services/Widget/WidgetRegistryTest.php (2)
src/Cms/Services/Widget/WidgetRegistry.php (2)
  • WidgetRegistry (15-87)
  • getAll (61-64)
src/Cms/Services/Content/ShortcodeParser.php (1)
  • ShortcodeParser (15-178)
tests/Integration/CategoryTagRelationshipTest.php (1)
tests/Integration/IntegrationTestCase.php (2)
  • IntegrationTestCase (35-285)
  • createTestUser (243-267)
tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (1)
src/Cms/Services/Media/CloudinaryUploader.php (1)
  • CloudinaryUploader (16-197)
tests/Integration/PostPublishingFlowTest.php (1)
tests/Integration/IntegrationTestCase.php (2)
  • IntegrationTestCase (35-285)
  • createTestUser (243-267)
tests/Unit/Cms/Repositories/DatabasePageRepositoryTest.php (1)
src/Cms/Repositories/DatabasePageRepository.php (1)
  • DatabasePageRepository (16-181)
tests/Unit/Cms/Services/Content/EditorJsRendererTest.php (2)
src/Cms/Services/Content/EditorJsRenderer.php (1)
  • EditorJsRenderer (12-241)
src/Cms/Services/Content/ShortcodeParser.php (1)
  • ShortcodeParser (15-178)
tests/Integration/UserAuthenticationFlowTest.php (1)
tests/Integration/IntegrationTestCase.php (2)
  • IntegrationTestCase (35-285)
  • createTestUser (243-267)
tests/Integration/TagManagementTest.php (1)
tests/Integration/IntegrationTestCase.php (2)
  • IntegrationTestCase (35-285)
  • createTestUser (243-267)
tests/Unit/Cms/Repositories/DatabasePasswordResetTokenRepositoryTest.php (2)
src/Cms/Models/PasswordResetToken.php (1)
  • PasswordResetToken (14-178)
src/Cms/Repositories/DatabasePasswordResetTokenRepository.php (1)
  • DatabasePasswordResetTokenRepository (19-117)
tests/Unit/Cms/Services/Email/SenderTest.php (1)
src/Cms/Services/Email/Sender.php (10)
  • Sender (17-271)
  • to (40-44)
  • cc (49-53)
  • bcc (58-62)
  • subject (67-71)
  • body (76-81)
  • replyTo (86-91)
  • attach (96-100)
  • template (105-132)
  • send (137-197)
🪛 LanguageTool
UPGRADE_NOTES.md

[style] ~44-~44: The double modal “Required Run” is nonstandard (only accepted in certain dialects). Consider “to be Run”.
Context: ... Changes - None ### Action Required 1. Run php neuron cms:upgrade to copy new mi...

(NEEDS_FIXED)

tests/TESTING.md

[uncategorized] ~237-~237: The official name of this software platform is spelled with a capital “H”.
Context: ...tion (GitHub Actions) The CI workflow (.github/workflows/ci.yml) runs **both unit and...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
tests/TESTING.md

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 PHPMD (2.15.0)
tests/Unit/Cms/Services/Widget/WidgetRegistryTest.php

186-186: Avoid unused parameters such as '$name'. (undefined)

(UnusedFormalParameter)

tests/Integration/CategoryTagRelationshipTest.php

36-36: Avoid unused local variables such as '$parentId'. (undefined)

(UnusedLocalVariable)


40-40: Avoid unused local variables such as '$childId'. (undefined)

(UnusedLocalVariable)

tests/Unit/Cms/Services/Widget/WidgetTest.php

16-16: Avoid unused parameters such as '$attrs'. (undefined)

(UnusedFormalParameter)

tests/Unit/Cms/Repositories/DatabasePageRepositoryTest.php

218-218: Avoid unused local variables such as '$page1'. (undefined)

(UnusedLocalVariable)

src/Cms/Controllers/Traits/UsesDtos.php

53-53: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)

tests/Integration/TagManagementTest.php

222-222: Avoid unused local variables such as '$unusedTagId1'. (undefined)

(UnusedLocalVariable)


225-225: Avoid unused local variables such as '$unusedTagId2'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (89)
tests/Unit/Cms/ContentControllerTest.php (1)

6-6: LGTM! Namespace update aligns with project-wide refactoring.

The import path has been correctly updated from Neuron\Data\Setting\Source\Memory to Neuron\Data\Settings\Source\Memory, reflecting the namespace reorganization from singular to plural. The change is consistent with the broader namespace realignment mentioned in the PR objectives.

tests/Unit/Cms/Maintenance/MaintenanceConfigTest.php (1)

6-6: and

UPGRADE_NOTES.md (4)

149-149: Verify /CLAUDE.md is a legitimate documentation file.

Line 149 references /CLAUDE.md as a help resource. This filename is unconventional and may be a debug artifact or internal documentation. Clarify whether this file:

  • Is intended for end users
  • Should be included in the upgrade guidance
  • Should be renamed or removed

50-50: Verify MIGRATIONS.md exists and is accessible.

The document references MIGRATIONS.md in lines 50, 149, and 151 as the migration best practices guide. Ensure this file:

  • Exists in the repository
  • Is properly documented
  • Is linked or discoverable from the upgrade process

Also applies to: 149-149, 151-151


140-143: Verify version dates align with release timeline.

The Version History table shows:

  • Version 2025.12.5 with release date 2025-12-05
  • Current date is 2025-12-19

The version 2025.12.5 release date is in the past relative to the PR creation date (2025-11-29), which is inconsistent. Verify that version dates accurately reflect the intended or actual release dates.


96-96: Verify if cms:upgrade --check flag is actually implemented in UpgradeCommand.

Line 96 references a --check flag for cms:upgrade, and line 113 references a --fake flag for database migrations. While Neuron CMS uses robmorgan/phinx (^0.16) for database migrations, which supports the --fake flag as a standard feature, the existence and implementation of the --check flag in UpgradeCommand requires direct code inspection to confirm. Verify in the UpgradeCommand class that these flags are properly defined and documented.

tests/phpunit.xml (1)

10-13: LGTM! Test migration complete and properly organized.

The separation of unit and integration tests into distinct directories follows best practices. All 57 unit tests and 7 integration tests have been successfully migrated with no orphaned tests remaining, and PHPUnit is correctly configured to discover tests in both directories.

tests/Unit/Cms/Services/RegistrationServiceTest.php (1)

10-11: Namespace migration is correct and verified complete.

The update from Neuron\Data\Setting\... to Neuron\Data\Settings\... on lines 10-11 is part of a successful namespace reorganization across the entire codebase. Verification confirms no lingering references to the old Setting namespace exist, and the new Settings namespace is consistently used in 25+ production files including services, repositories, controllers, and initializers. The test file imports align with the production code, including RegistrationService.php which uses the same new namespaces.

tests/Unit/Cms/Repositories/DatabaseCategoryRepositoryTest.php (1)

8-8: LGTM! ORM integration is correctly implemented.

The addition of ORM initialization via Model::setPdo() properly configures the test environment to work with ORM-backed repository operations. The initialization is correctly placed before repository construction.

Also applies to: 33-34

tests/Unit/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (1)

8-8: LGTM! Namespace update aligns with project refactoring.

The import path has been correctly updated from Neuron\Data\Setting\SettingManager to Neuron\Data\Settings\SettingManager, consistent with the namespace reorganization across the PR.

tests/Unit/Cms/Services/AuthenticationTest.php (1)

11-12: LGTM! ORM and namespace updates are correctly implemented.

The namespace change to Neuron\Data\Settings\SettingManager and ORM initialization via Model::setPdo() are properly configured for the test environment.

Also applies to: 50-51

tests/Unit/Cms/Repositories/DatabaseUserRepositoryTest.php (1)

8-9: LGTM! ORM integration is correctly configured.

The namespace update and ORM initialization are properly implemented, following the consistent pattern across the test suite.

Also applies to: 47-48

tests/Unit/Cms/Repositories/DatabasePasswordResetTokenRepositoryTest.php (1)

1-253: LGTM! Comprehensive test coverage for password reset tokens.

This new test file provides excellent coverage of the DatabasePasswordResetTokenRepository:

  • Creation and persistence of tokens
  • Token retrieval and expiration handling
  • Deletion operations (by email, by token, expired tokens)
  • Edge cases (nonexistent tokens, multiple tokens per email)

The test structure is clear with good use of helper methods and proper assertions.

tests/Unit/Cms/Repositories/DatabaseTagRepositoryTest.php (1)

8-8: LGTM! ORM integration follows established pattern.

The ORM initialization is correctly implemented and consistent with other repository tests in this PR.

Also applies to: 33-34

tests/Unit/Cms/Repositories/DatabasePostRepositoryTest.php (1)

10-10: LGTM! ORM initialization is correctly configured.

The addition of ORM support follows the consistent pattern established across the test suite.

Also applies to: 35-36

tests/Unit/Cms/Services/Email/SenderTest.php (1)

178-192: Constructor tests look good.

The constructor variation tests appropriately verify that the Sender class can be instantiated with different parameter combinations without throwing exceptions. This is a reasonable level of coverage for constructor flexibility.

tests/Unit/Cms/Services/PasswordResetterTest.php (2)

43-43: Path correction appears accurate for the current file location.

The update from 3 to 4 directory levels correctly reflects the file's depth within the project structure (tests/Unit/Cms/Services/). The $_basePath is used by PasswordResetter for resource access (line 52), so verify that templates and other resources load correctly with this path.


11-12: Verify the namespace changes against the library version.

The namespace updates from Setting to Settings require confirmation against neuron-php/data 0.9.0 source code. Web documentation shows conflicting references to both singular and plural forms of the namespace, and the test directory structure references Setting/ (singular). Ensure the imports match the actual implementation in the specific library version being used.

src/Cms/Auth/Filters/CsrfFilter.php (1)

68-73: Good security improvement using filtered input.

The refactoring from direct $_POST access to Post::filterScalar('csrf_token') enhances security by applying input filtering. The truthy check on line 70 will work correctly: when csrf_token is missing from POST data, filter_input() returns null, which is falsy in PHP, allowing the header fallback at lines 75-79 to execute as intended. The return type compatibility is sound—filterScalar returns mixed, which includes null values acceptable for the conditional flow.

tests/Unit/Cms/Services/Dto/DtoFactoryServiceTest.php (1)

17-17: Path is correct and resolves properly.

The path adjustment successfully points to src/Cms/Dtos. The directory exists and contains all expected DTO files (CreateCategoryDto, CreatePostDto, CreateUserDto, MediaUploadDto, RegisterUserDto, UpdateCategoryDto, UpdatePostDto, UpdateUserDto).

tests/Unit/Cms/Services/Media/MediaValidatorTest.php (1)

20-34: Test setup is well-structured.

The virtual filesystem and in-memory settings configuration properly isolate the tests and align with the MediaValidator's expected dependencies.

tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (1)

17-29: Test setup is appropriate.

The in-memory settings configuration correctly provides all required Cloudinary credentials for testing.

tests/Unit/Cms/BlogControllerTest.php (3)

13-14: LGTM!

The namespace updates from Setting to Settings are consistent with the broader namespace reorganization mentioned in the PR summary.


16-16: LGTM!

The ORM Model import is necessary for the ORM initialization on line 55 and aligns with the PR's migration to the Neuron ORM.


129-129: The content_raw default value is correct.

EditorJS automatically creates a default block during initialization when the blocks array is empty, so the '{"blocks":[]}' default is appropriate for this column and matches EditorJS's expected JSON format.

tests/Unit/Cms/Repositories/DatabasePageRepositoryTest.php (12)

14-18: LGTM! Clean test class structure.

The class properties are well-defined for testing with an in-memory database and repository instance.


19-41: LGTM! Proper test environment setup.

The setUp method correctly initializes an in-memory SQLite database, configures the ORM, and creates the repository instance. The use of Model::setPdo() aligns with the ORM-based approach mentioned in the repository implementation.


43-87: LGTM! Comprehensive schema definition.

The table schemas correctly define all necessary fields for users and pages, including the foreign key constraint for referential integrity.


89-112: LGTM! Well-designed test helpers.

The helper methods effectively reduce duplication and use uniqid() to ensure unique test data. The EditorJS JSON format in the content field aligns with the PR's block-based editor upgrade.


132-148: LGTM! Proper coverage of slug lookup.

Both successful lookup and non-existent slug scenarios are correctly tested.


150-189: LGTM! Comprehensive create operation tests.

Both successful creation and duplicate slug prevention are properly tested with appropriate assertions and exception expectations.


191-228: LGTM! Thorough update operation coverage.

All three update scenarios are properly tested:

  • Successful update
  • Update without ID
  • Duplicate slug prevention

Note: The static analysis hint about $page1 being unused on line 218 is a false positive. The variable is intentionally created to occupy the 'page-one' slug, which is necessary to test duplicate slug detection.


230-248: LGTM! Complete delete operation testing.

Both successful deletion and non-existent record scenarios are correctly verified.


286-314: LGTM! Proper testing of status-specific retrieval methods.

Both getPublished() and getDrafts() are correctly tested with verification of both count and status values.


316-345: LGTM! Comprehensive author filtering tests.

Both basic author filtering and combined author+status filtering are properly verified.


347-369: LGTM! Complete count operation testing.

Both total count and status-filtered count scenarios are properly tested.


371-388: LGTM! Proper view count increment testing.

Both successful increment with persistence and non-existent record handling are correctly verified.

tests/Unit/Cms/Services/Widget/WidgetRendererTest.php (2)

12-42: Excellent coverage of error and edge cases.

The first three test methods properly validate defensive behavior: unknown widgets, missing dependencies, and empty states. Mock expectations and assertions are well-structured.


134-153: Strong XSS protection validation.

The test properly verifies that potentially malicious HTML is escaped, preventing script injection vulnerabilities. This is critical for user-generated content rendering.

tests/Unit/Cms/Services/Widget/WidgetTest.php (2)

93-119: Proper temporary file cleanup in tests.

Both view rendering tests correctly create temporary files and clean them up with unlink(), preventing test pollution. This demonstrates good test hygiene.


121-179: Comprehensive HTML sanitization test coverage.

The test suite thoroughly validates the sanitization allowlist, covering safe tags (div, p, strong), dangerous tags (script, iframe), and various content elements (links, images, headings, lists). The inline comment at line 137-138 helpfully documents strip_tags behavior.

tests/Unit/Cms/Services/Widget/WidgetRegistryTest.php (2)

12-126: Thorough coverage of registry lifecycle operations.

The test suite comprehensively validates widget registration, unregistration, retrieval, and edge cases. Mock expectations properly verify ShortcodeParser integration, ensuring both the registry and parser stay synchronized.


172-197: Elegant callback capture pattern for integration testing.

This test uses an advanced technique to capture the callback registered with ShortcodeParser and verify it correctly invokes the widget's render method. This validates the end-to-end integration between WidgetRegistry and widget rendering.

tests/TESTING.md (1)

1-300: Excellent testing documentation!

This comprehensive testing guide provides clear structure, practical examples, and helpful troubleshooting tips. The organization by test type (unit vs. integration), detailed coverage breakdown, and CI integration notes make this a valuable resource for contributors.

.github/workflows/ci.yml (4)

29-36: Clarify the fail_ci_if_error: true change.

Setting fail_ci_if_error: true means the CI pipeline will fail if Codecov upload encounters any errors (network issues, authentication problems, etc.). This is stricter than the previous configuration and could cause CI failures unrelated to code quality.

Consider whether this strict setting is appropriate for your workflow. If Codecov availability is critical, keep it true. If you want CI to succeed even with coverage upload failures, set it to false.

Current setting causes CI to fail on Codecov errors. Is this the intended behavior?


1-36: Well-structured CI workflow with comprehensive test coverage.

The separation of unit and integration test suites with dedicated coverage reporting provides good visibility into code quality. The use of pcov for coverage and the structured approach to testing aligns well with the testing guide documentation.


13-14: No action needed. The workflow correctly uses shivammathur/setup-php@v2, which is the recommended rolling tag that automatically receives all bug fixes, security patches, and new feature releases.


13-18: Remove the reference to Phinx — it is not a project dependency.

PHP 8.4 was released in November 2024, and while testing with this version in CI is reasonable, ensure that actual dependencies (PHPUnit 9.*, Neuron ORM, PHPMailer, Cloudinary PHP) are compatible. Note that composer.json does not specify an explicit PHP version constraint, which may complicate dependency resolution across environments.

Likely an incorrect or invalid review comment.

tests/Unit/Cms/Services/EmailVerifierTest.php (2)

40-40: Base path correction is accurate and properly fixes a bug.

The base path calculation has been updated from 3 directory levels up to 4 levels up. Since the test file is located at tests/Unit/Cms/Services/EmailVerifierTest.php, the old path would resolve to the tests/ directory, while the new path correctly resolves to the project root. This is a necessary fix because EmailVerifier uses the base path to locate email templates via the Sender service, and the email templates are stored at resources/views/emails/ in the project root, not in the tests directory.


10-11: Namespace refactoring to Settings is correct for neuron-php/data 0.9.x.

The project correctly requires neuron-php/data 0.9.*, and the updated imports from Neuron\Data\Settings\Source\Memory align with the current API. The namespace change from singular Setting to plural Settings is part of the library's standardized naming convention in version 0.9.x.

src/Cms/Controllers/Traits/UsesDtos.php (4)

8-8: LGTM! Clean import alias.

The RequestMapper alias improves code readability throughout the trait.


15-16: LGTM! Documentation accurately reflects the new behavior.

The PHPDoc clearly communicates that input filtering is now handled through RequestMapper.


43-46: Verify PHPDoc claim about RequestMapper's sanitization method.

The PHPDoc states RequestMapper "applies proper input sanitization via Post::filterScalar()". RequestMapper is from the external neuron-php/dto dependency, and this specific implementation detail cannot be verified in this codebase. Confirm the claim is accurate against the actual RequestMapper source, or remove/generalize the statement to avoid misleading documentation about an external package's internal behavior.


53-65: Verify neuron-php/dto library version 0.0. and RequestMapper.mapFiltered() method.*

The implementation logic is sound with appropriate conditional handling, but the external dependency requires attention. The project uses neuron-php/dto version 0.0.*, a pre-release version. While the RequestMapper.map() method is documented, the mapFiltered() method called on line 61 is not clearly documented in public sources. Before merging, confirm that:

  1. Version 0.0.* is production-ready or if a stable release should be required
  2. The RequestMapper.mapFiltered() method exists and functions as expected
tests/Unit/Cms/Services/Post/UpdaterTest.php (3)

34-72: Well-structured test for Editor.js content migration.

The test correctly validates both getContentRaw() (the raw JSON) and getBody() (the extracted text content), ensuring the content transformation pipeline works correctly. The assertion callback in the mock verifies all expected fields are set before the repository update is called.


74-102: LGTM!

Slug generation tests correctly use Editor.js JSON content format and validate the expected slug derivation behavior.


293-363: Good coverage for publishedAt timestamp handling.

Tests correctly validate that:

  1. publishedAt is set when transitioning to published status
  2. Existing publishedAt is preserved on subsequent updates

This ensures the original publication date is never overwritten.

tests/Unit/Cms/Services/Post/CreatorTest.php (2)

35-70: Thorough validation of Editor.js content handling.

The test properly validates the content transformation by checking:

  • getContentRaw() matches the input JSON
  • getBody() contains the extracted text ("Test body content")

This ensures the Post model correctly parses Editor.js blocks.


72-98: LGTM!

Slug generation test correctly validates that slugs are derived from titles when not explicitly provided.

tests/Unit/Cms/Services/Content/EditorJsRendererTest.php (4)

9-47: Comprehensive test suite for EditorJsRenderer.

Good coverage of base cases including empty data and empty blocks arrays. The paragraph test validates the essential HTML structure.


82-105: Good edge case coverage for header level clamping.

Tests validate that invalid header levels (0, 10) are properly clamped to the valid range (1-6). This matches the implementation in EditorJsRenderer::renderHeader().


338-364: Critical XSS sanitization tests.

Good coverage of security-sensitive behavior:

  • Script tags are stripped (XSS prevention)
  • Safe inline tags (<strong>, <em>) are preserved

This validates the sanitizeHtml() implementation works correctly.


366-382: ShortcodeParser integration test.

The mock properly validates that when content contains a shortcode bracket [, the parser is invoked. This tests the conditional logic in parseInlineContent().

tests/Unit/Cms/Services/Page/CreatorTest.php (3)

13-52: Thorough test for Page creation with all parameters.

The test validates all configurable fields including meta tags (title, description, keywords), template, and content. Good use of named parameters for clarity.


137-162: Excellent slug generation edge case coverage.

Tests cover important edge cases:

  • Multiple spaces → single hyphen
  • Special characters stripped
  • Leading/trailing whitespace trimmed

This ensures robust slug generation behavior.


164-210: Good deterministic testing for fallback slugs.

Using a mocked IRandom with predictable return values (abc123, xyz789) ensures tests are deterministic. This properly tests the fallback behavior for non-ASCII and symbol-only titles.

tests/Unit/Cms/Services/Page/UpdaterTest.php (7)

1-11: LGTM! Clean test structure.

The test class is well-organized with appropriate imports and namespace. The setup follows PHPUnit best practices.


12-49: LGTM! Comprehensive test coverage.

This test thoroughly validates the update operation with all parameters, correctly mocking the repository and asserting all expected state changes including the updatedAt timestamp.


77-101: LGTM! Good coverage for publishing behavior.

This test correctly verifies that transitioning a page to published status sets the publishedAt timestamp. The same consistency suggestion regarding ->with($page) applies here as well.


103-127: LGTM! Important test for date preservation.

This test correctly verifies that updating an already-published page preserves the original publishedAt timestamp, which is crucial for maintaining accurate publication history.


129-150: LGTM! Good validation of timestamp behavior.

This test properly verifies both the presence and type of the updatedAt timestamp, ensuring immutability is maintained.


152-173: LGTM! Good error handling coverage.

This test correctly verifies that the updater propagates repository failures, which is essential for proper error handling in the service layer.


1-174: Excellent test coverage overall.

This test file provides comprehensive coverage of the Updater service with well-structured test cases covering happy paths, edge cases, and error scenarios. The tests follow PHPUnit best practices and are clearly named.

The only minor inconsistency is the use of ->with($page) mock constraint in the first test but not in others. While not critical, adding this constraint throughout would provide more rigorous verification that the correct Page object is passed to the repository.

tests/Integration/CategoryTagRelationshipTest.php (1)

1-538: Well-structured integration test suite for category/tag relationships.

The tests comprehensively cover:

  • CRUD operations for categories and tags
  • Many-to-many relationships via pivot tables
  • Uniqueness constraints with proper expectException usage
  • Cascade delete behavior
  • Combined category and tag relationships on posts

The transactional isolation from IntegrationTestCase ensures test independence.

tests/Integration/IntegrationTestCase.php (1)

35-285: Solid integration test infrastructure.

The base class provides:

  • Multi-database support (SQLite, MySQL, PostgreSQL)
  • PID-based SQLite file paths for parallel test isolation
  • One-time migration execution with per-test transaction isolation
  • Useful helper methods for test data creation

Good foundation for the integration test suite.

tests/Integration/PostPublishingFlowTest.php (3)

143-203: Test overlaps with CategoryTagRelationshipTest.

The testPostWithCategories and testPostWithTags methods here test similar functionality to methods in CategoryTagRelationshipTest. Consider whether this duplication is intentional for testing different flows, or if these could be consolidated.

This is acceptable if the intent is to test relationships within the publishing flow context specifically.


270-314: Good test of foreign key cascade behavior.

This test validates that the author_id foreign key on posts is configured with ON DELETE CASCADE. If the migration is misconfigured, this test will catch it—exactly what integration tests should verify.


1-388: Comprehensive post publishing flow tests.

The test suite covers the complete post lifecycle including creation, updates, publishing, deletion, relationships, constraints, and transaction isolation. Well-documented with clear step numbering in testCompletePostPublishingFlow.

tests/Integration/UserAuthenticationFlowTest.php (2)

67-203: Good token security practices demonstrated in tests.

The email verification and password reset token tests correctly demonstrate:

  • Storing only hashed tokens in the database (SHA-256)
  • Token expiration handling
  • Token cleanup after use

This matches security best practices for token management.


1-422: Thorough authentication flow integration tests.

The test suite covers essential authentication scenarios:

  • User registration and password verification
  • Email verification token lifecycle
  • Password reset flow with secure token handling
  • Expired token cleanup
  • Login attempt tracking and account lockout
  • Username and email uniqueness constraints
  • Role and status persistence

Well-structured tests that validate both data persistence and security mechanisms.

tests/Integration/PostPublishingSchedulingTest.php (2)

301-308: Realistic simulation of view count increments.

While a single UPDATE posts SET view_count = 10 would be more efficient, the loop correctly simulates how view tracking would work in production—validating that the atomic view_count = view_count + 1 pattern works correctly across multiple operations.


1-536: Comprehensive scheduling and publishing state tests.

The test suite provides excellent coverage of:

  • Status transitions (draft → published → draft)
  • Scheduled post handling with future/past time comparisons
  • Auto-publishing logic for due scheduled posts
  • View count tracking
  • Status-based and time-based queries
  • Published date preservation on updates

The testFindScheduledPostsReadyToPublish test is particularly valuable for validating scheduled publishing cron job logic.

tests/Integration/PageManagementFlowTest.php (1)

1-22: LGTM! Well-structured integration test suite.

The test class provides comprehensive coverage of the page management workflow including creation, updates, publishing, and cascade deletes. The use of direct PDO operations for test data setup is appropriate for integration tests.

tests/Integration/TagManagementTest.php (3)

1-21: LGTM! Comprehensive tag management test coverage.

The test class covers a wide range of tag-related scenarios including creation, resolution, usage tracking, and cascade deletes. The docblock accurately describes the test coverage.


420-424: Same pattern: unused variables $orphan1Id and $orphan2Id could be simplified.

Similar to the previous finding, if you need to verify by ID (as done on line 439), keeping them is fine. However, the assertion message would be clearer if you kept track of what you're verifying.


505-516: Consider verifying array keys after array_filter.

After array_filter, the array keys are preserved. If downstream code relies on sequential 0-indexed keys, you'd need array_values. The current assertions on lines 514-515 will still pass because $tagNames[0] and $tagNames[1] retain their original keys, but this is a subtle detail worth noting.

tests/Unit/Cms/Services/CsrfTokenTest.php (3)

5-5: LGTM! Good testability improvement.

The addition of FakeRandom dependency injection enables deterministic testing of CSRF token generation, which improves test reliability and makes token generation verifiable.

Also applies to: 18-18


25-31: LGTM! Proper test fixture setup.

The setUp() correctly initializes FakeRandom with a deterministic seed and injects it into CsrfToken, enabling consistent and repeatable test execution.


59-73: LGTM! Correctly tests token uniqueness with deterministic RNG.

The test properly verifies that different seeds produce different tokens, maintaining the intent of the original test while using deterministic FakeRandom.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
UPGRADE_NOTES.md (1)

11-11: Standardize command invocation syntax throughout the document.

The document inconsistently uses both ./vendor/bin/neuron (lines 11, 17, 22) and php neuron (lines 44, 45, 82, 96-98, 107, 110, 132). This creates confusion for users following the upgrade instructions.

🔎 Recommended fix

Choose one format and apply consistently. I recommend php neuron as it's more common in PHP CLI documentation:

 1. **Run the upgrade command:**
    ```bash
-   ./vendor/bin/neuron cms:upgrade
+   php neuron cms:upgrade
    ```

 2. **Review and apply migrations:**
    The upgrade command will detect new migrations. Review them and run:
    ```bash
-   ./vendor/bin/neuron db:migrate
+   php neuron db:migrate
    ```

 3. **Clear caches:**
    ```bash
-   ./vendor/bin/neuron cache:clear  # if applicable
+   php neuron cache:clear  # if applicable
    ```

Apply the same pattern to all command references throughout the document.

Based on past review comments flagging this same issue.

Also applies to: 44-45, 82-82, 96-98, 107-107, 110-110, 132-132

🧹 Nitpick comments (10)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)

21-28: Consider a more maintainable approach to path detection.

The nested dirname() calls (5 levels deep) are brittle and will break if the directory structure changes. Consider using __DIR__ with a known relative path or a configuration constant.

🔎 Alternative approach
 public function __construct()
 {
 	// Get project root (where composer.json is)
 	$this->_projectPath = getcwd();
 
 	// Get component path
-	$this->_componentPath = dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) );
+	// From src/Cms/Cli/Commands/Install/UpgradeCommand.php, go up 5 levels to package root
+	$this->_componentPath = realpath( __DIR__ . '/../../../../../' );
 }

Using realpath() with a relative path is more explicit and easier to understand than counting dirname() calls.

MIGRATIONS.md (1)

307-310: Add language specifiers to fenced code blocks.

For better syntax highlighting and markdown compliance, specify the language for the fenced code blocks showing example terminal output.

🔎 Proposed fix

At line 307:

-```
+```text
 ✅ 20251205120000_add_two_factor_recovery_codes_to_users.php
 ❌ 20251205120000_update_users.php

At line 407:
```diff
-```
+```text
 SQLSTATE[HY000]: General error: 1 duplicate column name
</details>


Also applies to: 407-409

</blockquote></details>
<details>
<summary>src/Cms/Auth/Filters/AuthCsrfFilter.php (1)</summary><blockquote>

`109-118`: **Consider using a response object instead of direct HTML output.**

The `respondForbidden` method directly outputs HTML and calls `exit`, which tightly couples the filter to a specific response format. While this works and properly uses `htmlspecialchars` to prevent XSS, consider refactoring to use a response object or template system for better separation of concerns and testability.



This is a minor point and can be deferred if the direct output approach is consistent with the rest of the codebase.

</blockquote></details>
<details>
<summary>src/Cms/Repositories/DatabasePostRepository.php (1)</summary><blockquote>

`386-401`: **Consider wrapping multiple INSERTs in a transaction.**

The loop on lines 395-398 performs multiple INSERTs without a transaction. If one fails mid-loop, you'll have partial category attachments. Consider wrapping the foreach loop in a transaction for atomicity.



<details>
<summary>🔎 Proposed fix</summary>

```diff
 public function attachCategories( int $postId, array $categoryIds ): bool
 {
 	if( empty( $categoryIds ) )
 	{
 		return true;
 	}

+	$this->_pdo->beginTransaction();
+	try
+	{
 		$stmt = $this->_pdo->prepare( "INSERT INTO post_categories (post_id, category_id, created_at) VALUES (?, ?, ?)" );
 		$now = ( new \DateTimeImmutable() )->format( 'Y-m-d H:i:s' );
 		foreach( $categoryIds as $categoryId )
 		{
 			$stmt->execute( [ $postId, $categoryId, $now ] );
 		}
+		$this->_pdo->commit();
+	}
+	catch( \Exception $e )
+	{
+		$this->_pdo->rollBack();
+		throw $e;
+	}

 	return true;
 }

Apply the same pattern to attachTags (lines 417-432).

src/Cms/Models/Post.php (2)

125-128: Consider logging JSON decode failures.

While the fallback to ['blocks' => []] is safe, silently ignoring malformed JSON could make debugging difficult. Consider logging decode failures for troubleshooting.

🔎 Suggested enhancement
 public function getContent(): array
 {
-	return json_decode( $this->_contentRaw, true ) ?? ['blocks' => []];
+	$decoded = json_decode( $this->_contentRaw, true );
+	if( $decoded === null && json_last_error() !== JSON_ERROR_NONE )
+	{
+		// Optional: log warning for debugging
+		// error_log( "Failed to decode content_raw: " . json_last_error_msg() );
+	}
+	return $decoded ?? ['blocks' => []];
 }

142-147: Consider validating JSON in setContent.

While setContentArray explicitly validates and throws JsonException on encoding failure, setContent accepts any string without validation. Invalid JSON will fail silently in getContent(). For consistency and early error detection, consider validating the JSON in setContent.

🔎 Suggested enhancement
 public function setContent( string $jsonContent ): self
 {
+	// Validate JSON
+	$decoded = json_decode( $jsonContent, true );
+	if( $decoded === null && json_last_error() !== JSON_ERROR_NONE )
+	{
+		throw new \JsonException( "Invalid JSON content: " . json_last_error_msg() );
+	}
 	$this->_contentRaw = $jsonContent;
 	$this->_body = $this->extractPlainText( $jsonContent );
 	return $this;
 }
src/Cms/Maintenance/MaintenanceManager.php (2)

70-81: Event emission logic is correct.

The implementation correctly stores the result and emits the event only on success. Consider adding use statements at the top of the file for cleaner code instead of fully qualified class names.

📋 Optional refactor: Add use statements

Add these at the top of the file after line 5:

 use DateTime;
+use Neuron\Application\CrossCutting\Event;
+use Neuron\Cms\Events\MaintenanceModeEnabledEvent;
+use Neuron\Cms\Events\MaintenanceModeDisabledEvent;

Then simplify the event emissions:

-		\Neuron\Application\CrossCutting\Event::emit( new \Neuron\Cms\Events\MaintenanceModeEnabledEvent(
+		Event::emit( new MaintenanceModeEnabledEvent(
 			$data['enabled_by'],
 			$message
 		) );

206-209: Good defensive programming with JSON error handling.

The added JSON error check prevents issues with corrupted maintenance files. Consider logging the error for better observability when debugging maintenance mode issues in production.

tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (1)

148-157: Consider consolidating duplicate IP test.

This test verifies the same IP address (169.254.169.254) as testUploadFromUrlRejectsLinkLocalIp (lines 137-146). Since IP validation doesn't consider the URL path, these tests are functionally identical. Consider removing this test or consolidating both into a single test with a more general name.

🔎 Proposed consolidation

Rename the test at lines 137-146 to be more comprehensive:

-	public function testUploadFromUrlRejectsLinkLocalIp(): void
+	public function testUploadFromUrlRejectsLinkLocalAndCloudMetadataIp(): void
 	{
 		$uploader = new CloudinaryUploader( $this->_settings );
 
 		$this->expectException( \Exception::class );
 		$this->expectExceptionMessage( 'private or reserved IP address' );
 
-		// 169.254.0.0/16 is link-local (APIPA)
+		// 169.254.0.0/16 is link-local (APIPA) and commonly used for cloud metadata services (e.g., 169.254.169.254)
 		$uploader->uploadFromUrl( 'https://169.254.169.254/image.jpg' );
 	}

And remove the duplicate test at lines 148-157.

tests/Unit/Cms/ContentControllerTest.php (1)

43-49: Consider refactoring to avoid real filesystem access.

The test creates a real file in the parent directory, which can cause issues with parallel test execution and CI/CD environments. While the virtual filesystem (vfsStream) is already set up, it's not used for the version file because ContentController loads from a hardcoded path.

Consider one of these approaches:

  • Make the version file path configurable in ContentController via dependency injection
  • Mock the file loading mechanism in ContentController
  • Use a test-specific configuration that points to the virtual filesystem

This would improve test isolation and eliminate filesystem side effects.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8060076 and cd4eb7c.

📒 Files selected for processing (25)
  • MIGRATIONS.md (1 hunks)
  • UPGRADE_NOTES.md (1 hunks)
  • resources/app/Initializers/AuthInitializer.php (3 hunks)
  • resources/config/routes.yaml (1 hunks)
  • src/Cms/Auth/Filters/AuthCsrfFilter.php (1 hunks)
  • src/Cms/Cli/Commands/Install/UpgradeCommand.php (1 hunks)
  • src/Cms/Dtos/CategoryFieldsDto.yaml (1 hunks)
  • src/Cms/Dtos/CreateCategoryDto.yaml (1 hunks)
  • src/Cms/Dtos/CreatePostDto.yaml (1 hunks)
  • src/Cms/Dtos/CreateUserDto.yaml (1 hunks)
  • src/Cms/Dtos/PostFieldsDto.yaml (1 hunks)
  • src/Cms/Dtos/RegisterUserDto.yaml (1 hunks)
  • src/Cms/Dtos/UpdateCategoryDto.yaml (1 hunks)
  • src/Cms/Dtos/UpdatePostDto.yaml (1 hunks)
  • src/Cms/Dtos/UserCredentialsDto.yaml (1 hunks)
  • src/Cms/Events/MaintenanceModeEnabledEvent.php (1 hunks)
  • src/Cms/Maintenance/MaintenanceManager.php (2 hunks)
  • src/Cms/Models/Post.php (5 hunks)
  • src/Cms/Repositories/DatabasePostRepository.php (13 hunks)
  • src/Cms/Services/Content/EditorJsRenderer.php (1 hunks)
  • src/Cms/Services/Media/CloudinaryUploader.php (1 hunks)
  • tests/Unit/Cms/ContentControllerTest.php (5 hunks)
  • tests/Unit/Cms/Models/PostTest.php (1 hunks)
  • tests/Unit/Cms/Services/Content/EditorJsRendererTest.php (1 hunks)
  • tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • src/Cms/Dtos/UpdatePostDto.yaml
  • src/Cms/Dtos/RegisterUserDto.yaml
  • src/Cms/Dtos/UpdateCategoryDto.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/app/Initializers/AuthInitializer.php
  • tests/Unit/Cms/Services/Content/EditorJsRendererTest.php
🧰 Additional context used
🧬 Code graph analysis (8)
src/Cms/Auth/Filters/AuthCsrfFilter.php (2)
src/Cms/Services/Auth/Authentication.php (1)
  • Authentication (19-382)
src/Cms/Services/Auth/CsrfToken.php (1)
  • CsrfToken (17-75)
src/Cms/Events/MaintenanceModeEnabledEvent.php (2)
src/Cms/Events/MaintenanceModeDisabledEvent.php (1)
  • getName (35-38)
src/Cms/Events/UserLoginEvent.php (1)
  • getName (39-42)
src/Cms/Maintenance/MaintenanceManager.php (2)
src/Cms/Events/MaintenanceModeEnabledEvent.php (1)
  • MaintenanceModeEnabledEvent (23-40)
src/Cms/Events/MaintenanceModeDisabledEvent.php (1)
  • MaintenanceModeDisabledEvent (24-39)
tests/Unit/Cms/Models/PostTest.php (1)
src/Cms/Models/Post.php (2)
  • setContent (142-147)
  • getBody (108-111)
tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (1)
src/Cms/Services/Media/CloudinaryUploader.php (3)
  • CloudinaryUploader (16-326)
  • upload (67-88)
  • uploadFromUrl (98-117)
src/Cms/Repositories/DatabasePostRepository.php (5)
src/Cms/Models/Post.php (6)
  • fromArray (488-585)
  • setCategories (376-380)
  • setTags (436-440)
  • toArray (592-609)
  • setId (65-69)
  • incrementViewCount (321-325)
src/Cms/Models/Tag.php (3)
  • fromArray (125-156)
  • toArray (163-172)
  • setId (44-48)
src/Cms/Models/Category.php (3)
  • fromArray (143-175)
  • toArray (182-192)
  • setId (45-49)
src/Cms/Models/PasswordResetToken.php (3)
  • fromArray (133-163)
  • toArray (168-177)
  • setId (48-52)
src/Cms/Repositories/DatabasePageRepository.php (4)
  • create (50-65)
  • count (154-164)
  • all (101-118)
  • incrementViewCount (169-180)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)
src/Cms/Cli/Commands/Install/InstallCommand.php (1)
  • __construct (25-32)
src/Cms/Services/Media/CloudinaryUploader.php (2)
src/Cms/Controllers/Admin/Media.php (2)
  • Media (18-192)
  • __construct (26-39)
src/Cms/Services/Media/IMediaUploader.php (3)
  • upload (22-22)
  • uploadFromUrl (32-32)
  • delete (41-41)
🪛 LanguageTool
UPGRADE_NOTES.md

[style] ~44-~44: The double modal “Required Run” is nonstandard (only accepted in certain dialects). Consider “to be Run”.
Context: ... Changes - None ### Action Required 1. Run php neuron cms:upgrade to copy new mi...

(NEEDS_FIXED)

🪛 markdownlint-cli2 (0.18.1)
MIGRATIONS.md

307-307: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


407-407: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 PHPMD (2.15.0)
src/Cms/Cli/Commands/Install/UpgradeCommand.php

61-61: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (67)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (2)

174-178: Good fix from previous review!

The use of json_last_error() correctly addresses the previous concern about reliable JSON parse error detection.


407-427: Good fix from previous review!

The unused $installedMigrations variable has been correctly removed, and the code now directly uses $packageMigrations.

MIGRATIONS.md (1)

1-464: Excellent comprehensive migration guide!

This document provides thorough coverage of migration principles, workflows, and best practices. The examples are clear and the troubleshooting section is particularly helpful.

UPGRADE_NOTES.md (1)

29-52: Well-structured version documentation.

The version-specific upgrade information is clear, comprehensive, and actionable. The inclusion of migration principles and references to detailed documentation (MIGRATIONS.md) is particularly helpful for users performing upgrades.

resources/config/routes.yaml (1)

254-265: CSRF protection now applied to media upload routes.

The new routes correctly apply the auth-csrf filter, addressing the previous review concern about missing CSRF protection. However, the AuthCsrfFilter implementation (reviewed separately) has a critical bug that must be fixed for these routes to function properly.

Note: The past review comment mentioned filter: auth csrf (space-separated), but the implementation uses filter: auth-csrf (hyphenated), which is the correct filter name for the composite filter.

src/Cms/Auth/Filters/AuthCsrfFilter.php (4)

1-26: LGTM!

The class structure, documentation, and property declarations are well-designed. The exempt methods list (GET, HEAD, OPTIONS) correctly identifies safe HTTP methods that don't require CSRF protection.


27-44: LGTM!

The constructor properly implements dependency injection with sensible defaults. The closure-based approach for the parent Filter constructor is appropriate.


58-77: LGTM!

The CSRF validation logic correctly exempts safe HTTP methods and properly validates tokens for state-changing requests. Logging of security events is appropriate.


79-98: Post::filterScalar is an external API from the neuron-php/data dependency and cannot be verified within this repository.

Line 85 uses \Neuron\Data\Filters\Post::filterScalar('csrf_token') which is defined in the external neuron-php/data (0.9.*) package, not in this codebase. While the API is used consistently across the codebase (also in CsrfFilter.php) and documented as applying "proper input sanitization," verification of its actual implementation requires inspection of the external package source code, which is outside the scope of this repository.

src/Cms/Services/Content/EditorJsRenderer.php (2)

91-106: LGTM! Clean refactoring.

The delegation to renderListItem improves code organization and sets up proper handling for nested list structures.


108-153: LGTM! Well-structured nested list handling.

The method correctly handles both legacy string format and nested list objects. The recursive approach properly processes nested structures with appropriate indentation.

src/Cms/Repositories/DatabasePostRepository.php (10)

9-9: Namespace updated to match project refactoring.

The import path change from Neuron\Data\Setting\SettingManager to Neuron\Data\Settings\SettingManager aligns with the broader namespace reorganization mentioned in the PR summary.


14-34: LGTM! Documentation accurately reflects hybrid approach.

The class documentation and constructor comments clearly indicate the ORM-based approach while noting that raw SQL is retained for methods like getByCategory and getByTag that require JOINs.


39-110: LGTM! Good use of eager loading pattern.

The new loadRelations method effectively prevents N+1 query issues by eagerly loading categories and tags. The use of raw SQL for JOINs is appropriate given the current ORM limitations noted in the code.


115-147: LGTM! Transaction properly wraps all operations.

The use of Post::transaction on line 124 ensures atomicity across post creation and relation synchronization, addressing the previous review concern about partial updates.


152-182: LGTM! Update operation properly transactional.

The transaction wrapping ensures atomic updates to both the post and its relations.


187-193: LGTM! Clean ORM-based deletion.

The deletion logic is straightforward and correctly returns a boolean. The comment about cascade deletes is helpful context.


198-323: LGTM! Query methods properly migrated to ORM.

The migration to ORM-based queries is well-executed. The TODO comments for getByCategory and getByTag appropriately note the need for future JOIN support in the ORM QueryBuilder.


325-339: LGTM! Race condition properly addressed.

The atomic increment operation on lines 334-336 correctly addresses the previous review concern about lost increments under concurrency. The documentation clearly explains the fix.


341-360: LGTM! Transaction concern addressed at caller level.

While syncCategories doesn't have its own transaction, it's properly called within Post::transaction blocks in both create (line 135) and update (line 174) methods, addressing the previous review concern about atomicity.


362-381: LGTM! Consistent with syncCategories pattern.

The syncTags method mirrors the syncCategories implementation and is similarly called within transaction boundaries in create (line 142) and update (line 178).

src/Cms/Dtos/CreateUserDto.yaml (1)

1-23: LGTM! Documentation clarifies DTO design decision.

The added header comment clearly explains why the flat structure is used instead of composition, referencing the RequestMapper limitation. This is helpful for future maintainers who might wonder about the DTO structure.

src/Cms/Dtos/CreateCategoryDto.yaml (1)

1-20: LGTM! Consistent documentation pattern.

The header comment follows the same pattern as other DTOs in this PR, maintaining consistency across the codebase.

src/Cms/Dtos/CreatePostDto.yaml (1)

1-36: LGTM! Documentation maintains consistency.

The header comment is consistent with the pattern established in other Create*Dto files.

src/Cms/Dtos/UserCredentialsDto.yaml (1)

1-34: LGTM! Well-documented reference DTO.

The extensive documentation clearly explains the purpose, current limitations, and future usage path for this reusable DTO. The field definitions are appropriate for user credentials with reasonable validation constraints.

src/Cms/Dtos/CategoryFieldsDto.yaml (1)

1-27: LGTM! Reusable category fields definition.

The DTO follows the same pattern as UserCredentialsDto, providing a reusable field definition for category-related operations. Documentation is clear and field constraints are reasonable.

src/Cms/Dtos/PostFieldsDto.yaml (1)

1-43: LGTM! Comprehensive post fields definition.

The DTO captures all essential post fields with appropriate validation constraints. The documentation is consistent with other reference DTOs in this PR.

tests/Unit/Cms/Models/PostTest.php (5)

323-345: LGTM!

Test correctly verifies extraction of plain text from a simple list with string items.


347-379: LGTM!

Test correctly verifies extraction of plain text from nested lists, including both simple string items and object items with nested content.


381-418: LGTM!

Test correctly verifies extraction from multi-level nested lists and includes the critical assertion that "Array" strings do not appear in the output, addressing the concern from previous reviews.


420-464: LGTM!

Test provides excellent coverage of mixed content extraction, including headers, paragraphs, and nested lists in a single content structure.


466-504: LGTM!

Test correctly verifies that HTML tags are stripped while preserving the text content, which is important for both security and plain-text rendering.

src/Cms/Models/Post.php (4)

21-22: LGTM!

Sensible defaults for both properties. The empty Editor.js JSON structure ensures consistent behavior when no content is set.


599-599: LGTM!

Addition of content_raw to the array representation is consistent with the new property and necessary for serialization.


617-656: LGTM!

The plain text extraction logic is well-implemented. The use of extractListText for list blocks (line 639) properly addresses the nested list handling concern from previous reviews, preventing "Array" strings in the output.


668-697: LGTM! Past review concern resolved.

This method correctly implements the nested list extraction logic suggested in previous reviews. It properly handles:

  • Simple string items
  • Object items with content property
  • Recursive extraction of nested items arrays

The implementation aligns with the Editor.js List plugin v1.9+ nested structure and prevents "Array" strings in the output.

src/Cms/Events/MaintenanceModeEnabledEvent.php (1)

1-40: LGTM! Clean event implementation.

The event class is well-structured and thoroughly documented. The implementation correctly follows the IEvent interface pattern and is consistent with the related MaintenanceModeDisabledEvent.

src/Cms/Maintenance/MaintenanceManager.php (1)

97-107: Good implementation: User captured before deletion.

The logic correctly captures the disabling user before deleting the maintenance file, preventing loss of this information. The event is properly emitted only on successful file removal.

src/Cms/Services/Media/CloudinaryUploader.php (10)

27-31: LGTM!

The constructor properly injects dependencies and initializes the Cloudinary instance. Exception handling is clearly documented.


39-57: LGTM!

The initialization method properly validates required Cloudinary configuration and provides a clear error message directing users to the config file.


67-88: LGTM!

The upload method properly validates file existence, handles errors gracefully, and provides clear error messages with context.


98-117: LGTM! SSRF protection implemented.

The uploadFromUrl method now includes comprehensive SSRF protection via validateUrlAgainstSsrf, addressing the security concern raised in the previous review. The implementation properly validates URLs before passing them to Cloudinary.


126-139: LGTM!

The delete method correctly uses Cloudinary's destroy API and properly interprets the result status.


154-207: LGTM! Comprehensive SSRF protection.

The URL validation method implements robust SSRF protection:

  • HTTPS-only requirement
  • URL format and parsing validation
  • IP address validation for both direct IPs and resolved hostnames
  • Blocking of private, loopback, link-local, and cloud metadata ranges

Note: DNS rebinding (TOCTOU) is a theoretical concern where DNS could change between validation and Cloudinary's actual request, but this is a known limitation of DNS-based SSRF protection and difficult to mitigate without controlling the HTTP client directly. The current implementation follows industry best practices.


215-246: LGTM!

The DNS resolution method properly handles both IPv4 and IPv6 records and gracefully returns an empty array when resolution fails (which is handled appropriately by the caller).


263-271: LGTM!

The IP validation method correctly uses PHP's built-in flags to detect private and reserved IP ranges for both IPv4 and IPv6.


279-305: LGTM!

The options builder properly merges user-provided options with config defaults and uses sensible fallbacks.


313-325: LGTM!

The result formatter properly normalizes Cloudinary's response into a consistent structure and correctly prefers secure_url (HTTPS) over url.

tests/Unit/Cms/Services/Media/CloudinaryUploaderTest.php (15)

17-29: LGTM!

The test setup uses in-memory settings, ensuring tests are isolated and don't depend on external configuration files. This is a best practice for unit testing.


31-41: LGTM!

The test properly verifies that the constructor throws an exception when Cloudinary configuration is missing, ensuring proper validation at initialization.


43-51: LGTM!

The test correctly verifies that the upload method validates file existence before attempting to upload.


53-61: LGTM!

The test properly verifies that malformed URLs are rejected.


63-71: LGTM!

The test properly verifies the HTTPS-only security requirement for URL uploads.


73-81: LGTM!

The test verifies that non-HTTPS protocols like FTP are also rejected, ensuring comprehensive protocol validation.


83-95: LGTM!

The test properly handles system-dependent DNS resolution behavior for localhost while ensuring the security goal (blocking localhost) is achieved in all cases. The flexible assertion pattern is well-documented.


97-105: LGTM!

The test verifies that loopback addresses (127.0.0.1) are blocked, preventing SSRF attacks to localhost services.


107-115: LGTM!

The test verifies that the private IP range 10.0.0.0/8 is blocked, preventing SSRF to internal networks.


117-125: LGTM!

The test verifies that the private IP range 192.168.0.0/16 is blocked.


127-135: LGTM!

The test verifies that the private IP range 172.16.0.0/12 is blocked, completing coverage of all RFC1918 private ranges.


137-146: LGTM!

The test verifies that link-local addresses (169.254.0.0/16) are blocked. This range is also significant for cloud environments where it may be used for instance metadata services.


164-180: LGTM!

The integration test stub is well-documented and provides clear guidance for enabling integration testing when needed. Marking it as incomplete is appropriate for the unit test suite.


182-195: LGTM!

The integration test stub properly documents requirements for enabling real URL upload testing.


197-209: LGTM!

The integration test stub for the delete operation is well-documented and appropriately marked as incomplete.

tests/Unit/Cms/ContentControllerTest.php (4)

19-40: LGTM! Clean refactoring from properties to local variables.

The refactoring from class properties ($this->root, $this->settings, $this->router) to local variables is appropriate since none of the test methods directly reference these values. This improves test clarity and reduces unnecessary instance state.


59-59: Good addition for test isolation.

Adding DtoFactoryService to the Registry cleanup ensures proper isolation between tests. This aligns with the cleanup patterns introduced elsewhere in the PR.


171-171: Appropriate annotation for session-related tests.

Adding @preserveGlobalState disabled to tests that use @runInSeparateProcess and interact with SessionManager is correct. This prevents global state pollution issues with PHP sessions.

Also applies to: 194-194


6-6: Verify the namespace exists in the neuron-php/data dependency.

The import statement references Neuron\Data\Settings\Source\Memory from the external neuron-php/data package (0.9.*). This namespace change from Neuron\Data\Setting\Source\Memory (singular) to Neuron\Data\Settings\Source\Memory (plural) appears across multiple test files. The actual class definition should be verified by checking the external package documentation or running composer install to confirm the namespace is correct in the dependency version being used.

Comment on lines +92 to +93
// Get who is disabling before we delete the file
$disabledByUser = $disabledBy ?? get_current_user();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Searching for enable() method calls ==="
rg -n -C3 '->enable\(' --type=php

echo -e "\n=== Searching for disable() method calls ==="
rg -n -C3 '->disable\(' --type=php

echo -e "\n=== Searching for the MaintenanceManager class definition ==="
rg -n 'class MaintenanceManager' --type=php -A 5

echo -e "\n=== Searching for enable() and disable() method signatures ==="
rg -n 'public function (enable|disable)\(' --type=php -A 2

Repository: Neuron-PHP/cms

Length of output: 2500


Ensure $disabledBy parameter is always passed by callers.

The fallback to get_current_user() will return the PHP process owner (e.g., "www-data"), not the authenticated user, providing no meaningful audit trail. Since the parameter is optional, document that callers must always pass the actual user identifier to maintain accurate audit records.

🤖 Prompt for AI Agents
In src/Cms/Maintenance/MaintenanceManager.php around lines 92-93, remove the
fallback to get_current_user() and make the $disabledBy parameter required
(remove the default/optional behavior) and update the method docblock to state
callers must provide the authenticated user identifier; then update every call
site to pass the actual user id (search the codebase for this method, adjust
callers and tests accordingly) and delete the line that assigns $disabledByUser
using get_current_user().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
tests/Unit/Cms/BlogControllerTest.php (2)

54-56: Test isolation issue with static ORM state.

The Model::setPdo() call sets static state that persists across tests. This has already been flagged in a previous review. Consider adding cleanup in tearDown() to reset the PDO connection and prevent test interdependencies.


100-120: Missing test user data for referential integrity.

The users table is created but no test users are inserted, while test posts reference author_id (default = 1). This has already been flagged in a previous review. Consider inserting test user records to maintain referential integrity in the test environment.

🧹 Nitpick comments (1)
tests/Unit/Cms/BlogControllerTest.php (1)

290-360: Consider strengthening test assertions.

Multiple test methods only assert that the result is a string without verifying the actual behavior or content. For more robust tests, consider adding assertions that:

  • Verify the returned content contains expected post titles or data
  • Check that filtering logic correctly includes/excludes posts
  • Validate error handling for edge cases

This would improve test reliability and catch potential regressions more effectively.

Example: Strengthening assertions for testIndexReturnsPublishedPosts
public function testIndexReturnsPublishedPosts(): void
{
	$this->createTestPost( 'Published Post 1', 'published-1', Post::STATUS_PUBLISHED );
	$this->createTestPost( 'Published Post 2', 'published-2', Post::STATUS_PUBLISHED );
	$this->createTestPost( 'Draft Post', 'draft-1', Post::STATUS_DRAFT );

	$blog = $this->createBlogWithInjectedRepositories();
	$result = $blog->index( new Request() );

	$this->assertIsString( $result );
	// Verify published posts are present
	$this->assertStringContainsString( 'Published Post 1', $result );
	$this->assertStringContainsString( 'Published Post 2', $result );
	// Verify draft is not present
	$this->assertStringNotContainsString( 'Draft Post', $result );
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd4eb7c and 8a940d3.

📒 Files selected for processing (2)
  • src/Cms/Events/MaintenanceModeDisabledEvent.php (1 hunks)
  • tests/Unit/Cms/BlogControllerTest.php (4 hunks)
🔇 Additional comments (5)
src/Cms/Events/MaintenanceModeDisabledEvent.php (1)

1-38: LGTM! Previous issue has been resolved.

The unused User import flagged in the previous review has been removed. The current implementation is clean, well-documented, and correctly implements the IEvent interface. The use of constructor property promotion with readonly modifier is appropriate for an immutable event class.

tests/Unit/Cms/BlogControllerTest.php (4)

16-16: LGTM!

The Model import is correctly added to support ORM initialization in the test setup.


72-73: LGTM!

The path updates correctly reflect the new directory structure for the test environment.


129-129: LGTM!

The content_raw column with default Editor.js JSON structure correctly supports the block-based editor upgrade mentioned in the PR objectives.


13-14: Namespace update to Neuron\Data\Settings is correct and complete.

The imports have been properly updated from the singular Neuron\Data\Setting to the plural Neuron\Data\Settings namespace. The codebase is consistent throughout, with all references already migrated.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (3)

27-27: Refactor nested dirname() calls for maintainability.

Five nested dirname() calls are fragile and difficult to maintain. If the directory structure changes, this breaks silently. Consider using a more explicit path calculation or a constant.

🔎 Proposed refactor
-		$this->_componentPath = dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) );
+		// From src/Cms/Cli/Commands/Install/UpgradeCommand.php up to package root
+		$this->_componentPath = realpath( __DIR__ . '/../../../../../' );

Alternatively, define the component path as a constant in a bootstrap or configuration file.


61-61: Unused parameter can be removed if not required by parent.

The $parameters parameter is unused in this method. If the parent class Command doesn't require this signature, consider removing it for clarity.

🔎 Proposed refactor if allowed
-	public function execute( array $parameters = [] ): int
+	public function execute(): int

Note: Only apply this change if the parent class interface permits it.


435-448: Method name runMigrations() is misleading.

The method is named runMigrations() but doesn't actually run migrations—it only displays instructions to the user. This creates a false expectation for anyone reading the code at line 151 where it's called.

Consider renaming to displayMigrationInstructions() or promptMigrationRun() for clarity.

🔎 Proposed refactor
-	private function runMigrations(): bool
+	private function displayMigrationInstructions(): bool
 	{
-		$this->output->writeln( "Running migrations...\n" );
-
-		// For now, instruct user to run migrations manually
-		// In future, could integrate with MigrationManager
-
 		$this->output->info( "Run: php neuron db:migrate" );
 
 		return true;
 	}

And update the call site at line 151:

-			$this->runMigrations();
+			$this->displayMigrationInstructions();
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a940d3 and d771d95.

📒 Files selected for processing (1)
  • src/Cms/Cli/Commands/Install/UpgradeCommand.php (1 hunks)
🧰 Additional context used
🪛 PHPMD (2.15.0)
src/Cms/Cli/Commands/Install/UpgradeCommand.php

61-61: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)

394-403: Handle config file copy failures.

The copy() operation can fail, but failures are silently ignored. If a copy fails, the method continues and returns true, which is inconsistent with copyNewMigrations() that properly handles failures.

🔎 Proposed fix
 		foreach( $exampleFiles as $sourceFile )
 		{
 			$fileName = basename( $sourceFile );
 			$destFile = $configDest . '/' . $fileName;
 
-			if( copy( $sourceFile, $destFile ) )
+			if( !copy( $sourceFile, $destFile ) )
 			{
-				$this->output->writeln( "  ✓ Updated: $fileName" );
+				$this->output->warning( "  ✗ Failed to copy: $fileName" );
+				return false;
 			}
+			$this->output->writeln( "  ✓ Updated: $fileName" );
 		}
🧹 Nitpick comments (2)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (2)

140-141: Remove duplicate comment.

There are two consecutive "// Display summary" comments where only one is needed.

🔎 Proposed fix
-		// Display summary
 		// Display summary
 		$this->displaySummary();

61-61: Static analysis hint: unused parameter.

The $parameters argument is part of the Command interface but not used in this implementation. This is acceptable as it maintains interface compliance, though you could document it with @param array $parameters Unused in this command if desired.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d771d95 and 1f7c42a.

📒 Files selected for processing (1)
  • src/Cms/Cli/Commands/Install/UpgradeCommand.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)
src/Cms/Cli/Commands/Install/InstallCommand.php (2)
  • __construct (25-32)
  • displaySummary (910-925)
🪛 PHPMD (2.15.0)
src/Cms/Cli/Commands/Install/UpgradeCommand.php

61-61: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (4)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (4)

164-216: LGTM!

The manifest loading logic is well-implemented with proper error handling. The use of json_last_error() correctly detects JSON parse failures, and the fallback for old installations without a manifest is a nice touch.


305-360: LGTM!

The migration copy logic properly handles errors by checking the return value of copy() and returning false on failure. The validation of source file existence and directory creation is also well done.


365-375: LGTM!

The conservative approach of not automatically overwriting view files is prudent, as it preserves user customizations. Providing the package views location for manual comparison is helpful.


411-431: LGTM!

The manifest update logic is clean and correct. The unused variable issue from the previous review has been addressed, and error handling for file_put_contents() is properly implemented.

@ljonesfl ljonesfl closed this Dec 19, 2025
@ljonesfl ljonesfl deleted the feature/blog-editor-upgrade branch December 19, 2025 20:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (1)

394-410: Inconsistent error handling for configuration file copies.

When copy() fails (line 403), an error is displayed but the method still returns true (line 409). This is inconsistent with copyNewMigrations() which returns false on copy failure, and could allow the upgrade to appear successful despite missing configuration examples.

🔎 Proposed fix
+		$copyFailed = false;
+
 		foreach( $exampleFiles as $sourceFile )
 		{
 			$fileName = basename( $sourceFile );
 			$destFile = $configDest . '/' . $fileName;
 
 			if( copy( $sourceFile, $destFile ) )
 			{
 				$this->output->writeln( "  ✓ Updated: $fileName" );
 			}
 			else
 			{
 				$this->output->error( "  ✗ Failed to copy $fileName from $sourceFile to $destFile" );
+				$copyFailed = true;
 			}
 		}
 
-		return true;
+		return !$copyFailed;
src/Cms/Models/Post.php (2)

500-534: Previous feedback addressed.

The JSON encoding now properly checks for failures and throws JsonException with descriptive error messages. This is consistent with the setContentArray method behavior.


670-709: Previous feedback addressed - nested list extraction implemented.

The extractListText method now correctly handles both simple string items and nested list structures with content and items properties, matching the Editor.js List v1.9+ data structure.

src/Cms/Repositories/DatabasePageRepository.php (1)

169-179: Previous feedback addressed - atomic increment implemented.

The view count increment now uses an atomic ORM operation (increment('view_count', 1)) which avoids the race condition from the fetch-increment-save pattern.

🧹 Nitpick comments (8)
src/Cms/Cli/Commands/Install/UpgradeCommand.php (4)

27-27: Consider simplifying the dirname chain.

The nested dirname() calls (5 levels) are difficult to read and maintain. Consider using a more explicit approach for clarity.

🔎 Alternative approaches
-		$this->_componentPath = dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) );
+		// Go up 5 levels: src/Cms/Cli/Commands/Install -> root
+		$this->_componentPath = dirname( __DIR__, 5 );

Note: The second parameter to dirname() was added in PHP 7.0.


61-61: Unused $parameters parameter.

The $parameters parameter is defined but never used in the method body. If this signature is required by the parent Command class, consider adding a docblock comment to clarify, or remove the parameter if it's not needed.


140-141: Remove duplicate comment.

The comment "Display summary" appears twice consecutively.

🔎 Proposed fix
-		// Display summary
 		// Display summary
 		$this->displaySummary();

440-450: Method name doesn't match implementation.

The method runMigrations() doesn't actually run migrations—it only displays an instruction message. Consider renaming to displayMigrationInstructions() or similar to avoid confusion, or implement actual migration execution if that's the intent.

src/Cms/Models/Post.php (2)

122-128: Consider adding JSON_THROW_ON_ERROR for consistency.

The getContent() method uses json_decode with a fallback, but silently ignores decode errors. While the fallback is safe, consider using JSON_THROW_ON_ERROR for consistency with setContentArray() which throws on encoding errors.

🔎 Optional: Use try-catch for consistency
 public function getContent(): array
 {
-	return json_decode( $this->_contentRaw, true ) ?? ['blocks' => []];
+	try
+	{
+		$decoded = json_decode( $this->_contentRaw, true, 512, JSON_THROW_ON_ERROR );
+		return $decoded ?? ['blocks' => []];
+	}
+	catch( \JsonException $e )
+	{
+		return ['blocks' => []];
+	}
 }

647-657: Consider handling the checklist block type if using the standalone @editorjs/checklist plugin.

The standalone Editor.js checklist plugin stores items with text and checked properties, which differ from the list block structure. If checklists are or will be extracted as a separate block type (type: "checklist"), they won't be processed by the current match statement.

Note: If using Editor.js List 2.0, checklist is a style within the list block and is already handled by the 'list' case.

src/Cms/Repositories/DatabasePageRepository.php (1)

24-27: Consider removing unused $settings parameter or adding validation.

The constructor accepts SettingManager $settings but doesn't use it. This could confuse callers. Either remove the parameter (breaking change) or add a comment explaining it's kept for interface compatibility.

 public function __construct( SettingManager $settings )
 {
-	// No longer need PDO - ORM is initialized in Bootstrap
+	// Settings parameter kept for interface compatibility with IPageRepository
+	// ORM is initialized globally in Bootstrap
 }
src/Cms/Repositories/IUserRepository.php (1)

58-65: Consider using a more explicit return type for "not found" case.

Returning -1 for "user not found" is a magic number convention. Consider throwing an exception or using a nullable return type for clearer semantics.

Alternative approach:

/**
 * @return int New failed login attempts count
 * @throws UserNotFoundException if user not found
 */
public function incrementFailedLoginAttempts( int $userId ): int;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7c42a and 99b1918.

📒 Files selected for processing (13)
  • src/Cms/Auth/Filters/AuthCsrfFilter.php (1 hunks)
  • src/Cms/Cli/Commands/Install/UpgradeCommand.php (1 hunks)
  • src/Cms/Models/Post.php (5 hunks)
  • src/Cms/Repositories/DatabaseCategoryRepository.php (8 hunks)
  • src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (2 hunks)
  • src/Cms/Repositories/DatabasePageRepository.php (5 hunks)
  • src/Cms/Repositories/DatabasePasswordResetTokenRepository.php (2 hunks)
  • src/Cms/Repositories/DatabasePostRepository.php (13 hunks)
  • src/Cms/Repositories/DatabaseTagRepository.php (7 hunks)
  • src/Cms/Repositories/DatabaseUserRepository.php (5 hunks)
  • src/Cms/Repositories/IUserRepository.php (1 hunks)
  • src/Cms/Services/Auth/Authentication.php (4 hunks)
  • tests/Unit/Cms/Services/AuthenticationTest.php (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Cms/Repositories/DatabasePasswordResetTokenRepository.php
🧰 Additional context used
🧬 Code graph analysis (8)
src/Cms/Services/Auth/Authentication.php (7)
src/Cms/Events/UserLoginFailedEvent.php (1)
  • UserLoginFailedEvent (23-44)
src/Cms/Models/User.php (2)
  • isLockedOut (332-340)
  • isActive (187-190)
src/Cms/Repositories/DatabaseUserRepository.php (4)
  • incrementFailedLoginAttempts (154-169)
  • setLockedUntil (201-211)
  • resetFailedLoginAttempts (179-190)
  • findById (37-40)
src/Cms/Repositories/IUserRepository.php (4)
  • incrementFailedLoginAttempts (65-65)
  • setLockedUntil (82-82)
  • resetFailedLoginAttempts (73-73)
  • findById (17-17)
src/Cms/Auth/SessionManager.php (1)
  • set (99-103)
src/Cms/Events/UserLoginEvent.php (1)
  • UserLoginEvent (24-43)
src/Cms/Events/UserLogoutEvent.php (1)
  • UserLogoutEvent (24-41)
src/Cms/Repositories/DatabaseCategoryRepository.php (1)
src/Cms/Models/Category.php (3)
  • toArray (182-192)
  • setId (45-49)
  • getId (37-40)
src/Cms/Repositories/IUserRepository.php (2)
src/Cms/Repositories/DatabaseUserRepository.php (3)
  • incrementFailedLoginAttempts (154-169)
  • resetFailedLoginAttempts (179-190)
  • setLockedUntil (201-211)
src/Cms/Models/User.php (3)
  • incrementFailedLoginAttempts (296-300)
  • resetFailedLoginAttempts (305-310)
  • setLockedUntil (323-327)
src/Cms/Repositories/DatabaseTagRepository.php (2)
src/Cms/Repositories/DatabaseUserRepository.php (3)
  • create (69-90)
  • __sleep (216-220)
  • __wakeup (225-228)
src/Cms/Models/Tag.php (3)
  • toArray (163-172)
  • setId (44-48)
  • getId (36-39)
tests/Unit/Cms/Services/AuthenticationTest.php (6)
src/Cms/Repositories/DatabaseCategoryRepository.php (2)
  • __sleep (184-188)
  • __wakeup (193-196)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (2)
  • __sleep (139-143)
  • __wakeup (148-151)
src/Cms/Repositories/DatabasePasswordResetTokenRepository.php (2)
  • __sleep (121-125)
  • __wakeup (130-133)
src/Cms/Repositories/DatabasePostRepository.php (2)
  • __sleep (448-452)
  • __wakeup (457-460)
src/Cms/Repositories/DatabaseTagRepository.php (2)
  • __sleep (168-172)
  • __wakeup (177-180)
src/Cms/Repositories/DatabaseUserRepository.php (2)
  • __sleep (216-220)
  • __wakeup (225-228)
src/Cms/Repositories/DatabasePostRepository.php (5)
src/Cms/Models/Post.php (5)
  • fromArray (488-597)
  • getId (57-60)
  • toArray (604-621)
  • setId (65-69)
  • incrementViewCount (321-325)
src/Cms/Repositories/DatabaseCategoryRepository.php (4)
  • findBySlug (45-48)
  • create (77-98)
  • count (150-153)
  • all (142-145)
src/Cms/Repositories/DatabasePageRepository.php (5)
  • findBySlug (41-45)
  • create (50-65)
  • count (154-164)
  • all (101-118)
  • incrementViewCount (171-179)
src/Cms/Repositories/DatabaseTagRepository.php (4)
  • findBySlug (45-48)
  • create (61-82)
  • count (134-137)
  • all (126-129)
src/Cms/Repositories/DatabaseUserRepository.php (3)
  • create (69-90)
  • count (141-144)
  • all (133-136)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (6)
src/Cms/Repositories/DatabaseCategoryRepository.php (2)
  • __sleep (184-188)
  • __wakeup (193-196)
src/Cms/Repositories/DatabasePasswordResetTokenRepository.php (2)
  • __sleep (121-125)
  • __wakeup (130-133)
src/Cms/Repositories/DatabasePostRepository.php (2)
  • __sleep (448-452)
  • __wakeup (457-460)
src/Cms/Repositories/DatabaseTagRepository.php (2)
  • __sleep (168-172)
  • __wakeup (177-180)
src/Cms/Repositories/DatabaseUserRepository.php (2)
  • __sleep (216-220)
  • __wakeup (225-228)
tests/Unit/Cms/Services/AuthenticationTest.php (2)
  • __sleep (28-32)
  • __wakeup (34-37)
src/Cms/Repositories/DatabaseUserRepository.php (1)
src/Cms/Models/User.php (7)
  • toArray (413-433)
  • setId (69-73)
  • getId (61-64)
  • incrementFailedLoginAttempts (296-300)
  • getFailedLoginAttempts (279-282)
  • resetFailedLoginAttempts (305-310)
  • setLockedUntil (323-327)
🪛 PHPMD (2.15.0)
src/Cms/Cli/Commands/Install/UpgradeCommand.php

61-61: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (57)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (2)

7-7: LGTM! Namespace updated correctly.

The import path has been updated from Neuron\Data\Setting\SettingManager to Neuron\Data\Settings\SettingManager, which aligns with the project-wide namespace refactoring mentioned in the PR summary.


136-151: LGTM! Serialization pattern is consistent across repositories.

The __sleep() and __wakeup() methods correctly handle PHPUnit process isolation by preventing PDO serialization. This pattern matches the implementation in other repository classes (DatabaseCategoryRepository, DatabasePasswordResetTokenRepository, DatabasePostRepository, etc.).

Note that after unserialization, the $_pdo property will be null until re-initialized by test setup, which means the object temporarily cannot be used until external re-initialization occurs.

src/Cms/Cli/Commands/Install/UpgradeCommand.php (6)

175-196: Proper JSON error handling implemented.

The JSON parsing correctly uses json_last_error() to detect parse failures and includes detailed error messages via json_last_error_msg(). This handles edge cases where valid JSON (like null or {}) might otherwise be misinterpreted as parse errors.


415-435: LGTM: Manifest update with proper error handling.

The method correctly updates the manifest with package migrations and version info, properly checks the file_put_contents() return value, and returns a boolean result that's now checked by the caller (lines 135-138).


135-138: Proper handling of manifest update failure.

The return value of updateInstalledManifest() is now checked, and a warning is displayed if it fails. The approach of warning rather than failing the entire upgrade is reasonable since the actual file operations succeeded.


305-360: Excellent error handling in migration copy.

The method demonstrates proper error handling patterns:

  • Validates source files exist before copying
  • Checks copy() return values
  • Returns false immediately on failure
  • Provides clear success/failure feedback

This is a good reference pattern for file operations in this command.


221-289: Helper methods are well-structured.

The installation check, version display, and update detection logic are clear and correctly implemented:

  • isInstalled() validates multiple indicators
  • displayVersionInfo() provides clear version comparison
  • checkForUpdates() properly detects both migration and version changes
  • getNewMigrations() correctly uses array_diff to find new migrations

365-375: Conservative view update approach is appropriate.

The decision to require manual view updates rather than automatic copying is sensible, as it preserves user customizations. The informational output guides users on where to find package views if needed.

src/Cms/Models/Post.php (2)

21-22: LGTM!

The new properties _body and _contentRaw are correctly initialized with sensible defaults. The empty JSON structure {"blocks":[]} is a proper default for Editor.js content.


156-169: LGTM!

The setContentArray method correctly handles JSON encoding failures by checking for false and throwing a JsonException with a descriptive message. This addresses the previous review feedback about error handling.

src/Cms/Repositories/DatabaseTagRepository.php (4)

164-180: LGTM!

The __sleep and __wakeup methods correctly handle PHPUnit process isolation, matching the pattern used in other repositories (as shown in the relevant code snippets from DatabaseUserRepository, DatabaseCategoryRepository, etc.).


108-110: Verify save() returns boolean.

The ORM's save() method is expected to return bool and this expectation is accurate for standard Laravel Eloquent implementations. Ensure the model class being saved confirms to this contract.


75-82: Verify that the Tag model uses auto-incrementing primary key.

The code correctly assumes Tag::create() returns a model with a populated ID. Standard Laravel Eloquent behavior hydrates the auto-generated ID after insertion. However, verify that the tags table schema uses auto_increment on the ID column; if defined as a non-auto-incrementing primary key, the returned model may have id set to 0.


18-32: Verify if Neuron ORM provides method to access PDO connection from Model class.

The observation is accurate: $_pdo is used only in allWithPostCount(), and DatabasePageRepository demonstrates the ORM works without storing PDO. However, the suggestion to use Model::getPdo() or similar requires confirming that Neuron-PHP ORM exposes a method to retrieve the globally-set PDO connection for raw SQL queries.

src/Cms/Repositories/DatabasePageRepository.php (3)

34-35: LGTM!

Good use of eager loading with Page::with('author') to avoid N+1 queries when accessing author data.

Also applies to: 43-44


101-118: Both get() and all() are valid ORM methods. The codebase correctly uses query()->get() for methods with conditional parameters (status filter, limit, offset) and static Model::orderBy()->all() for simple cases without parameters. DatabasePageRepository follows the same pattern as DatabasePostRepository. No changes needed.


58-64: toArray() includes all ORM-managed fields—no verification needed.

The Page::toArray() method explicitly includes both created_at and updated_at timestamps along with all other fields. These are serialized to strings and can be properly deserialized by the ORM's hydrate mechanism. No data loss occurs.

Likely an incorrect or invalid review comment.

tests/Unit/Cms/Services/AuthenticationTest.php (2)

61-62: LGTM!

Correctly initializes the ORM with the PDO connection before running tests. This ensures the ORM-based repository methods work correctly.


91-91: LGTM!

The two_factor_recovery_codes column addition aligns with the broader 2FA feature support in the CMS.

src/Cms/Repositories/IUserRepository.php (1)

67-82: LGTM!

The resetFailedLoginAttempts and setLockedUntil methods are well-documented and provide atomic operations for account security management. The implementations in DatabaseUserRepository (shown in relevant snippets) correctly use atomic updates to avoid race conditions.

src/Cms/Services/Auth/Authentication.php (7)

49-56: LGTM! Event emission on user not found.

The event emission logic is correct, and the timing attack mitigation via dummy hash (line 48) is appropriately placed before the event.


64-70: LGTM! Event emission on locked account.

The event emission for locked accounts is correctly implemented with appropriate reason code.


77-83: LGTM! Event emission on inactive account.

Consistent with other failure events and correctly implemented.


90-107: LGTM! Atomic handling of failed login attempts.

The atomic increment and lock operations correctly prevent race conditions during concurrent login attempts. Event emission is appropriate.


111-115: LGTM! Atomic reset with user refresh.

The atomic reset followed by refreshing the user from the database ensures the in-memory User object reflects the updated state. Good pattern.


145-145: LGTM! Login time tracking and event emission.

The login time tracking enables session duration calculation at logout, and the event emission provides appropriate observability.

Also applies to: 152-158


166-203: LGTM! Logout with session duration and event emission.

The logout flow correctly calculates session duration, handles missing login_time gracefully, and only emits the event when a user was actually logged in.

src/Cms/Auth/Filters/AuthCsrfFilter.php (4)

49-56: LGTM! Authentication check is correct.

The authentication check correctly uses the check() method from the Authentication service. The previous issue with isAuthenticated() has been properly addressed.


58-76: LGTM! CSRF validation logic is sound.

The CSRF validation correctly exempts idempotent HTTP methods and properly validates tokens for state-changing requests.


82-98: LGTM! Token retrieval supports both form and AJAX requests.

The token retrieval correctly checks both POST data (with filtering) and HTTP headers, providing good support for different submission methods.


103-118: LGTM! Helper methods are correctly implemented.

The redirect and forbidden response helpers are properly implemented with appropriate security measures (htmlspecialchars) and execution control (exit).

src/Cms/Repositories/DatabaseCategoryRepository.php (8)

7-7: LGTM! Import path updated and PDO retention explained.

The import path change aligns with the broader ORM migration, and the PDO retention is appropriately documented for the raw SQL queries that remain.

Also applies to: 30-31


39-71: LGTM! ORM-based finder methods.

The ORM-based finder methods use standard patterns and are correctly implemented.


91-97: LGTM! ORM-based create with ID sync.

The create method correctly uses ORM and syncs the generated ID back to the input object, consistent with other repositories in the PR.


124-125: LGTM! ORM-based update.

The update method correctly delegates to the ORM's save operation.


134-136: LGTM! ORM-based delete.

The delete method correctly uses ORM and returns an appropriate boolean result.


144-144: LGTM! ORM-based all and count methods.

Both methods use standard ORM patterns and are correctly implemented.

Also applies to: 152-152


160-179: LGTM! Raw SQL with documented TODO for ORM migration.

The continued use of raw SQL for JOIN and aggregation is appropriately documented with a TODO, as the ORM doesn't yet support these operations. The query structure is correct.


181-196: LGTM! Serialization helpers for PHPUnit.

The serialization helpers correctly handle PDO connection lifecycle during test process isolation, consistent with other repositories in the PR.

src/Cms/Repositories/DatabasePostRepository.php (8)

41-74: LGTM! Post retrieval with eager relation loading.

The findById and findBySlug methods correctly retrieve posts and eagerly load their relations using the new loadRelations helper.


79-110: LGTM! Relation loading helper.

The loadRelations helper correctly loads categories and tags using JOIN queries and cleanly maps the results to model objects.


123-146: LGTM! Transactional create with relation syncing.

The create method correctly wraps post creation and relation syncing in a transaction, ensuring atomicity. This addresses the past review concern about partial failures.


166-181: LGTM! Transactional update with relation syncing.

The update method correctly uses a transaction to ensure atomic updates of the post and its relations, consistent with the create method and addressing past review concerns.


326-339: LGTM! Atomic view count increment.

The atomic increment implementation correctly addresses the race condition mentioned in the past review. The documentation clearly explains the approach.


344-381: LGTM! Relation sync helpers.

The sync helpers for categories and tags are correctly implemented. They are invoked within transactions in the create and update methods, ensuring atomicity and addressing the past review concern.


200-284: LGTM! Query methods with appropriate ORM usage.

The query methods correctly use ORM where applicable and fall back to raw SQL for JOIN operations with appropriate TODO comments indicating future migration paths.


446-460: LGTM! Serialization helpers for PHPUnit.

The serialization helpers are consistent with other repositories and correctly handle PDO connection lifecycle during test process isolation.

src/Cms/Repositories/DatabaseUserRepository.php (10)

7-7: LGTM! Import path updated and PDO made nullable.

The import path change and nullable PDO property align with the ORM migration. The backwards compatibility note is clear.

Also applies to: 20-20, 30-31


39-63: LGTM! ORM-based finder methods.

The finder methods correctly use ORM patterns consistent with other repositories in the PR.


83-89: LGTM! ORM-based create with ID sync.

The create method follows the same pattern as other repositories, correctly syncing the generated ID back to the input object.


116-117: LGTM! ORM-based update.

The update method correctly delegates to the ORM's save operation.


125-127: LGTM! ORM-based delete.

The delete method correctly uses ORM and returns an appropriate boolean result.


135-135: LGTM! ORM-based all and count methods.

Both methods use standard ORM patterns and are correctly implemented.

Also applies to: 143-144


146-169: LGTM! Atomic failed login attempts increment.

The atomic increment correctly prevents race conditions during concurrent login attempts. Fetching the updated count after increment is appropriate.


171-190: LGTM! Atomic failed login attempts reset.

The atomic reset correctly updates both failed_login_attempts and locked_until in a single operation, preventing race conditions.


192-211: LGTM! Atomic account lockout setter.

The atomic update for locked_until correctly prevents race conditions and properly handles DateTime formatting.


213-228: LGTM! Serialization helpers for PHPUnit.

The serialization helpers are consistent with other repositories and correctly handle PDO connection lifecycle during test process isolation.

Comment on lines +28 to +37
public function __sleep(): array
{
// Don't serialize PDO for process isolation
return ['_authentication', '_userRepository', '_sessionManager', '_passwordHasher'];
}

public function __wakeup(): void
{
// PDO will be re-initialized in setUp()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Serialization includes non-serializable objects.

The __sleep method returns fields that likely contain mocks (_sessionManager, _passwordHasher) and repository instances. These may not serialize properly. Consider returning an empty array like the repository classes do.

🔎 Suggested fix
 public function __sleep(): array
 {
 	// Don't serialize PDO for process isolation
-	return ['_authentication', '_userRepository', '_sessionManager', '_passwordHasher'];
+	return [];
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function __sleep(): array
{
// Don't serialize PDO for process isolation
return ['_authentication', '_userRepository', '_sessionManager', '_passwordHasher'];
}
public function __wakeup(): void
{
// PDO will be re-initialized in setUp()
}
public function __sleep(): array
{
// Don't serialize PDO for process isolation
return [];
}
public function __wakeup(): void
{
// PDO will be re-initialized in setUp()
}
🤖 Prompt for AI Agents
In tests/Unit/Cms/Services/AuthenticationTest.php around lines 28 to 37, the
__sleep method lists properties (_authentication, _userRepository,
_sessionManager, _passwordHasher) that may contain mocks or other
non-serializable objects; change __sleep to return an empty array so nothing is
serialized, leaving __wakeup as-is (PDO and other dependencies should be
re-initialized in setUp()), and run/update tests to ensure setup re-initializes
the needed properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants